Accessing the @import hierarchy at comptime

is there a way for my “root” main.zig module to access all other modules (AKA struct types) that are directly/indirectly added to the compilation via @import???  since types are involved, this (obviously) would occur at comptime

somewhat related, i noticed that @import requires a string literal – and not a comptime expression of type []const u8… were i able to create my own import function that basically delegates to @import while populating a comptime array on the side, my specific use-case can be handled…

bottom line – i want main.zig to locate all imported modules that have a particular trait that i can discover at comptime… thoughts on how to approach this???

Import used to be more flexible but then it was “nerfed” to make it easier for tooling to understand the import graph.

If by “imported modules” you mean actual Zig modules, then the answer lies in your build.zig file which is where you would wire in those modules in the first place. You can create at build time a module that contains a list of all other modules, and then do whatever you want with it at comptime.

If otherwise you meant “anything being imported” then afaik there is no way of doing that.

2 Likes

Take a look at this and see if anything seems relevant to your usecase: Zig Build System ⚡ Zig Programming Language

maybe…

let’s assume i have modules A, B, C, D… and assume that A->B, B->C, B->D, C->D where → means “imports”…

for sure, i can know about my universe of modules… but the hierarchic relationship between them is effectively encoded in the @import directives in their source files…

i can certainly express the “wiring graph” at some step in my build flow – but then everytime i change the graph (new module E, new dependencies, etc), would i be updating this information in two different places???

if i actually “generate” some stuff that each of my modules would import/embed, i suppose i can see my way through this… my goal was to (hopefully) do what i want in a single step of building main.zig

popping up a level, these modules are .zig files that follow a pattern imposed by a “framework” i’m trying to realize… besides this “imports” relation, there is actually other information that might need to be similarly captured…

said another way, imagine each module has a “spec” file (which itself could be expressed in zig) that is processed upstream from the actual compilation of the module’s “body”… the results of this processing would be constant data structures usable by the main.zig that actually drives the framework at both comptime as well as runtime…

i realize i’m thinking out loud, but there may be something in your original suggesrtion…

unlike #include in C, i probably shouldn’t assume anything about the order that zig @import calls are actually processed…

but the hierarchic relationship between them is effectively encoded in the @import directives in their source files…

Note that the heirarchic relationship is not fully encoded in source files, you also need any/all information that is generated in builtin.zig for every compilation which includes the target cpu/os/abi, whether you’re in test mode, the zig version, optimize settings, etc. Just because you see an @import("foo") doesn’t mean that the code depends on foo. Something from that module would first need to be used by code reached through an export (i.e. main), which changes depending on the current configuration. The std module contains many imports but many of them aren’t used unless you’re using a particular configuration.

For me I don’t think of import statements as establishing a heirachy relationship between modules. At the source level, all your doing is creating a “named slot” for someone to fill in for you. If you see @import("foo"), all that means is the current code is wanting you to provide something for you to use and “foo” is the slot name. The caller can shove any module they want into the “foo slot”. In fact, every module can @import("foo") and the configuration could put something different in every modules “foo slot”. There are real use cases where you will want to provide different things. For example, if a project “A” uses @import("log"), and calls @import("log").log("thing thing is", .{thing});, the module that gets placed in that slot named “log” could be vastly different depending on the context. This allows each project/configuration to provide their own “log” module that has a custom implementation. It doesn’t mean that “project A” depends on a project or module named “log”, it just means that A has a slot named “log” that might need to be filled in by the outer context. This system makes it easy to do the wrong thing, but it’s very simple/flexible, IMO it’s a good trade off in this situation.

1 Like

Imports in Zig are lazy so an @import statement actually doesn’t execute anything. It’s only when something is used from the imported structure that it performs the import.

Just theoretically, we could write a comptime function that does @embedFile with a file and then analyzes everything semantically at comptime and @embedFile for sub-imports and finally returns the “import map”. (yay :fearful:)

But, as marler8997 already said, we can’t directly find out that @import happened.

still playing in my sandbox, but i’ve already explored this @embedFile followed by comptime de-serialization technique you’ve suggested… if nothing else, it’s a way for an upstream build step to pass results downstream…

and if i understand @marler8997 correctly, the fact that @import requires a string literal (not an expression) implies that i need to bind that literal to some physical file upstream anyway…