Compilation of mutually-exclusive namespaces within a single source file

a variation on this earlier post

i have a single source that i want to compile in two distinct contexts, where a comptime constant will enable the compiler to determine which one is active…

as an implementation pattern, i will put all context-specific code into separate struct types contained in the source file; and i wiil then “import” one of these namespaces into the top level container…

something like this:

// common declarations used by both contexts

const self = @This;

const context = // A or B;
pub usingnamespace if (context == A) ContextA else ContextB;

pub fn main() void {
    self.init();      // available in either context
    if (context == A) {;
    else {;

pub const ContextA = struct {
    pub fn init() void { ... }
    pub fn foo() void { ... }

pub const ContextB = struct {
    pub fn init() void { ... }
    pub fn bar() void { ... }

within a context struct, top-level functions can call one another without “self”; and these functions can call any top-level file functions in the same way, since these are already statically in scope…

i recognize, however, that calling “into” the active context does require use of self

aside from general comments on this approach, my specific question is this: assuming these contexts are truly mutually-exclusive (and the compiler “knows” which is active), how much analysis of the unused context actually occurs???

for example, i was able to put a blatant 1 / 0 expression inside ContextB when ContextA is active – and i did NOT receive a compiler error!!!

from my perspective, this a great!!! but can i count on this #ifdef like behavior over time???

Lazy evaluation is basically a certainty at this point. I made a post about that today, actually: Comptime segfault on recursive types - #3 by AndrewCodeDev

The reason that compiler error in the example I linked to doesn’t actually fire is because that branch is never taken and gets thrown out. If you make it depend on runtime data, all branches get inspected and the compiler error fires. So not only does this happen at the level you’re talking about, it happens automatically even on switch/if-else branches.

For more info, I’d recommend looking into std.refAllDecls. It looks like they won’t keep that utility in the standard forever, but it’s what the utility does that may help clarify the behaviour you’re talking about. (for reference about why I said it may not stick around, check out: `std.testing.refAllDecls` references `pub` decls only, private ones are ignored · Issue #12838 · ziglang/zig · GitHub)

1 Like

To clarify a bit more here, I could have done exactly what you did in a different switch branch and had the same ifdef behaviour:

const std = @import("std");

fn traverse(comptime val: anytype) void {
    const T = @TypeOf(val);
    switch (@typeInfo(T)) {
        .Union => {
            if (@typeInfo(T).Union.tag_type) |Tag| {
                switch (@as(Tag, val)) {
                    inline else => |tag| {
                        traverse(@field(val, @tagName(tag)));
        .Pointer => {
            const y = 1 / 0;
            _ = &y;
        .Int => {
            @compileLog("In integer branch");
        else => {}

pub fn main() !void {
    const Union = union(enum) {
        int: u8,
        self: *const @This(),
    const val = Union{ .int = 10 };
    comptime traverse(val);

No error whatsoever until you hit the pointer branch - then we get the following:

example.zig:21:27: error: division by zero here causes undefined behavior


i had already discovered that i could wrap “context-specific” code in some sort of comptime condition… it was just starting to pollute my code…

my use-case involves a much more coarse-grained separation… effectively a framework that oversees compilation of each module within the workspace TWICE (ContextA and then ContextB), i should be able insert the pub usingnamespace stuff automatically…

1 Like