How to conditionally include/exclude functions from a namespace without usingnamespace?

I have some functions that are only available when multithreading is enabled. Right now I’m using usingnamespace to add them to the namespace conditionally. I’m wondering how that can be accomplished from now on. I haven’t actually gotten a chance to look into Zig 0.15.

A lot more ifs

Manually.

A bespoke, handcrafted, list of pub const name = container.name;.

Don’t forget to update it!

Oh wait I see. You’ll love this:

pub const blah = if (thing) 
    container.blah 
else 
    @compileError("No!");

Just repeat as often as you need. Easy peasy.

3 Likes

Is unconditionally including the functions an option?

I’m guessing they cause a compile error if you call them when you’re not supposed to.

3 Likes

Yeah, that causes problems when you programmatically scan through a namespace’s decls. Using @compileError() to indicate a function unavailability is generally problematic. Maybe an enum literal should be used instead in these situations?

const f = .@"Not available in single-threaded mode";

pub fn main() void {
    f();
}

That causes a compile error as before. Just to tweak the error message so that prints the literal instead of the current “type ‘@Type(.enum_literal)’ not a function”.

Or you could set f to be an actual function that simply does a @compileError with the message you want when called? Signature is an issue, but you could write a comptime helper function to build such a function from its meant-to-be-called counterpart in order to keep the signatures in sync.

That’s not possible, you can’t create a function like that at comptime. It’d need to be a code generation step and handled by the build system.

1 Like

A void value is often used in place of @compileError, that solves your problem and is already being done, even in std.

But I think (hope I’m not putting words in their mouth) that chung-leong would like the error message you get, if you accidentally try to use it in the wrong context, to be more specific.

I suppose that you could do something like this:

fn Unusable(comptime reason: []const u8) type {
    return struct {};
}

Then later on

const f = Unusable(“Not available in single threaded mode”);

Then at least if you try to call f you should see the reason in the error message since it’s part of the name of the type.

This requires a bit of tweaking to use,

fn Unusable(comptime reason: []const u8) type {
    _ = reason; // added to appease the unused variable gods
    return struct {};
}

// Note that they need to be instantiated{} as otherwise the error message
// will only say "error: type 'type' not a function"
const f = Unusable("not available in single threaded mode"){};
const g = Unusable("this is broken"){};

pub fn main() !void {
    _ = f;
    g();
}

There’s no error for the misuse of f, but g() gets the error message:

conditional.zig:11:5: error: type 'conditional.Unusable("not available in single threaded mode"[0..37])' not a function
    g();

Which is the wrong error entirely. The rules for when types are considered equivalent are a bit subtle, and I don’t know if I fully understand them, but it needs to be tweaked even further to use reason within the inner struct:

fn Unusable(comptime reason: []const u8) type {
    return struct {
        comptime {
            _ = reason;
        }
    };
}

Now the error message is correct:

conditional.zig:14:5: error: type 'conditional.Unusable("this is broken"[0..14])' not a function
    g();
3 Likes

How about something like this:

const UnusableOpaque = {};

pub fn isUnusable(value: anytype) bool {
    const T = @TypeOf(value);
    return switch (@typeInfo(T)) {
        .@"struct" => @hasDecl(T, "Opaque") and T.Opaque == UnusableOpaque,
        else => false,
    };
}

fn Unusable(comptime msg: []const u8) type {
    return struct {
        comptime reason: []const u8 = msg,

        pub const Opaque = UnusableOpaque;
    };
}

pub fn unusable(comptime msg: []const u8) Unusable(msg) {
    return .{};
}

const f = unusable("Not available in single-threaded mode");
const g = unusable("Deprecated");

pub fn main() void {
    @compileLog(comptime isUnusable(f));
    @compileLog(f.reason);
    g();
}
/home/cleong/Desktop/test.zig:36:5: error: type 'test.Unusable("Deprecated"[0..10])' not a function
    g();
    ^
/home/cleong/Desktop/test.zig:19:12: note: struct declared here
    return struct {
           ^~~~~~
referenced by:
    posixCallMainAndExit: /home/cleong/.zvm/0.14.1/lib/std/start.zig:651:22
    _start: /home/cleong/.zvm/0.14.1/lib/std/start.zig:468:40
    3 reference(s) hidden; use '-freference-trace=5' to see all references

Compile Log Output:
@as(bool, true)
@as([]const u8, "Not available in single-threaded mode"[0..37])

That allows you to detect at comptime whether something is an Unusable and also get the reason.

It’d be good to have something like this in the standard library. The “touch it and you die” problem happens a lot with translated C header files, where one untranslatable macro would make it impossible to introspect the namespace.

1 Like

I might need this kind of stuff as well for my program in a later phase.
Anyway… I like positive booleans. I would definitely write usable everywhere.
With unusable my brain has to switch to “not-mode” and be confused :slight_smile:

1 Like

Makes sense. The usable case is the one we generally act upon:

inline for (comptime std.meta.declarations(ns)) |decl| {
    const decl_value = @field(ns, decl.name);
    if (std.meta.isUsable(decl_value)) {
        // export the value or something
    }
}

A hasUsable() function would be useful too, I think:

pub fn hasUsable(comptime T: type, field_name: []const u8) bool {
    return @hasDecl(T, field_name) and isUsable(@field(T, field_name);
}

What if we had a builtin to check whether something causes a compile error?

inline for (comptime std.meta.declarations(ns)) |decl| {
    const decl_value = @usable(@field(ns, decl.name)) orelse continue;
    // export the value or something
}

I think if we had something like this together with compile-error-traces it would be quite neat, because then you could react to compile errors and avoid them or create new compile errors and shown a trace how the compile error changed along the way.

fn worksOnlyInMultithreading() void {
  if (builtin.single_threaded) @compileError("not available in single threaded mode");
  // ...
}

Or if you have metaprogramming that wants to be able to touch decls no matter what:

fn worksOnlyInMultithreading() void {
  if (builtin.single_threaded) @panic("not available in single threaded mode");
  // ...
}

Honestly anything more complicated than this would make me immediately discard a dependency.

3 Likes

void is not a suitable replacement for @compileError() for the purposes of indicating to the user that a decl (not the result of calling a function) is not/no longer usable. It results in nondescriptive compile errors like expected type 'usize', found 'void', without any compile error trace that can help the user find the file and line where the decl was declared.

Unusable("some error"[0..10]) is cute and an improvement over void but it still has the same problem in that it doesn’t show you the file and line it came from. I also think it would be awkward and highly unintuitive if users were expected to use some obscure type from std just so that they can emit compile error messages that don’t suck complete ass, when @compileError() sits right there.

I understand why @TypeOf(@compileError("")) can’t just yield noreturn, because handling compile-time errors differently in this context would complicate the language and there are also situations where you wouldn’t want the error to get swallowed. But maybe std.builtin.Type.Declaration could get extended with an is_noreturn: bool field? That way you would still be able to iterate over decls without risking compile errors:

inline for (@typeInfo(T).@"struct".decls) |decl| {
    if (!decl.is_noreturn) {
        const value = @field(T, decl.name);
    }
}
1 Like

I like the idea of distinct Unusable structs better. It more of a deliberate decision on the part of the developer. I’m purposely putting something there that is unusable. Inferring unusability from compilation errors risks inadvertent omissions. There are times when @compileError() points out actual coding problems.

Using a struct also means we can easily pack useful information into it. For example, functions often get renamed or moved to different part of the std namespace. It’d be useful to keep an Unusable at the old location with the address of the new location. Autodoc can then use this information to generate a link.

In the interest of better understanding the use case - why are you programmatically scanning through the namespace’s decls?

std.testing.refAllDecls is one example. From #19847:

Deprecated lib/std/std.zig decls were deleted instead of made a @compileError because the refAllDecls in the test block would trigger the @compileError. The deleted top-level std namespaces are:

  • std.rand (renamed to std.Random)
  • std.TailQueue (renamed to std.DoublyLinkedList)
  • std.ChildProcess (renamed/moved to std.process.Child)
1 Like