Style Question: Importing by module or source file

Let’s say i have a project structure like so:

- build.zig
- build.zig.zon
- src/
    - root.zig
    - ast/
           - objects.zig
           - parser.zig
    - lexer.zig
    - Token.zig

Now say I want to use lexer.zig in the parser.zig file. Under zig’s import semantics, I would need to add a module in the build.zig and make it importable. That’s great.

My question is this: Now say I want to import lexer.zig into my root.zig file.
What is the preferred method of import? @import("lexer.zig") or @import("lexer") since I already made it an importable module?

There is another option, which is also how the zig standard library handles this problem:
The standard library only has one module std which points to std.zig.
Inside of std.zig you’ll see a bunch of public declarations that allow you to access all the files.

Applying this to your case you would need to make a module for root.zig, then inside of root.zig you put all the files you want to access:

pub const lexer = @import("lexer.zig");
pub const Token = @import("Token.zig");
pub const ast = struct {
    pub const objects = @import("objects.zig");
    pub const parser = @import("parser.zig");
};
...

Then inside any source file you can access other files like this:

const root = @import("root_module");
const lexer = root.lexer;
const parser = root.ast.parser;
...
3 Likes

Isn’t the root module already available as @import("root")?

I am using the simplest method that works: @import("lexer.zig")

You have 3 ways to import a file. To me, they all mean something different:

std.Build.addModule should only be used when you intend you code to be used as a library. You are signaling that the associated root source file is an entry point for your library. A library can have multiple of these.

std.Build.addAnonymousModule creates a module, but does not expose it to dependents. It is effectively internal to your library. For me, this is best used to split large code cases in smaller units with designated responsabilities. I will never outright start a project with submodules, but instead split bigger projects when their size gets too big.

@import with a relative path is the final way to import a file. This means the file is internal to the module which uses it. It is also the simplest and recommended way for your use case.

Side note: If I recall correctly, modules cannot @import files from the parent folder of the root source file. Say you were to create an ast module for src/ast/ast.zig, it would not be able to access the src/lexer.zig module as the latter is in a parent directory of the former. This is a nice way to ensure that API boundaries are respected and modules cannot interact with internal APIs of other modules.

3 Likes

Yes, but I think if you export your library to another project root becomes the root of that project, like how the standard library uses @import("root") to read std_options from your main.zig. Because of that it would be more portable to create a new module.

1 Like

I see things like

in the standard library, haven’t tried it myself.

You can go up from deeper levels, but you can’t go up past where the root of the module starts.

Under zig’s import semantics, I would need to add a module in the build.zig and make it importable.

Huh? No you don’t. Just @import("../lexer.zig"). You can go up the directory tree (with ..) no problem, provided you don’t move above the dirname of the root source file (your main module’s root source file is src/root.zig, so the directory in question is src/).

The only case where you’d need a separate module here is if parser is a module itself (since then the module root directory is src/ast/ which lexer.zig is outside of), but I don’t see any point in performing such a separation here.

Now say I want to import lexer.zig into my root.zig file. What is the preferred method of import?

I’ll assume that you have modules root (at src/root.zig), parser (at src/ast/parser.zig), and lexer (at src/lexer.zig). If you try to do @import("lexer.zig") from within root.zig, then you will find out that Zig has made the decision for you: this raises a compile error.

Zig has a rule that every source file lives in precisely one module. If that rule didn’t exist, then @import("lexer.zig") would actually have to be a completely different thing to @import("lexer"), because code is compiled differently depending on the module it’s in (for instance, the results of @import depend on the current module’s import table). So, if this were allowed and you did it, your binary would end up with two copies of the lexer code in it! You’d also end up with weird errors about type mismatches between types which seem to be identical. Altogether, it would be a mess. So, we introduce this restriction to prevent people from accidentally doing things they didn’t mean to.

2 Likes

That’s fair, I forgot you could go up, as long as you don’t try to go back down again.

My question was really, “What form should I use” if both are possible, but if I’m reading correctly, as soon as I add src/lexer.zig to an anonymousImport (attacthed to the root module), then I will no longer be able to do @import("lexer.zig") anymore.

I keep local modules in a libs dir that is a sibling to src:
libs/somemod/somemod.zig
libs/othermod/othermod.zig
src/root.zig
src/stuff/stuff.zig
src/stuff/printer.zig
src/ui/ui.zig

That way your main app just has the application source and all the private modules it uses live in the lib dir, makes it simpler to see which parts were separated into modules and what is just part of the application directly.

So I use @import("somemod") for the libs modules and relative imports like @import("stuff/stuff.zig"), @import("printer.zig"), @import("../stuff/stuff.zig") for all the things that are below src.

3 Likes

Thanks for the replies, I’m going to mark @brodeuralexis answer as the solution, but there are a lot of good ideas in here that I like. In partcular i like the Idea of using a libs/ directory that is a sibling of the src/ directory

1 Like