Skip to content

Proposal: Prevent @typeInfo and @TypeOf from triggering compile errors #6620

Closed as not planned

Description

@SpexGuy

An important use case in Zig is the ability to load a module and use reflection to inspect the decarations it exposes. Unfortunately, it's also common to see modules that use pub const foo = @compileError(..); for deprecation, specialization, or other purposes like translation errors from translate-c. These two approaches are at odds with each other. This proposal suggests a way to keep the current use of @compileError and still inspect reflection data without triggering errors.

  1. For a basic fix, we need the ability to identify when a declaration fails to compile without actually failing the compile. To do this, we can add an element to TypeInfo with tag .CompileError.
  2. .CompileError should be an empty tag, because allowing the metaprogram to inspect the error string could cause difficult to find dependencies on compiler versions.
  3. We also need a special type @Type(.CompileError) which will be used in the decls of the type info of the containing struct.
  4. Creating a variable of type @Type(.CompileError) is a compile error.
  5. This would identify not just calls to @compileError but also declarations that are invalid for other reasons.
  6. Guarantee that calls to @TypeOf cannot cause a compile error. Instead, @TypeOf(<code that causes compile error>) returns .CompileError. This could be useful for a lot of use cases that are currently difficult, like
    • checking if an instance of one type can coerce to another type
    • checking if a function signature is compatible with a set of arguments
    • checking if a combination of argument types is valid for a given generic function

All of these use cases are technically solvable with just (1) through (5), but require the use of an intermediate struct whose type info can be inspected. This last point makes that use case a bit cleaner, and unifies the behaviors of "figuring out the type of a declaration for reflection" and "using @TypeOf".

Activity

  1. added
    proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.
    on Oct 9, 2020
  2. added this to the 0.8.0 milestone on Oct 9, 2020
  3. Rocknest commented on Oct 9, 2020

    @Rocknest
    Contributor

    By the way we already have noreturn.

    noreturn is the type of: break, continue, return, unreachable, while (true) {}

    @TypeOf(@compileError("")) == .NoReturn

  4. modified the milestones: 0.8.0, 0.9.0 on May 19, 2021
  5. jcmoyer commented on Jun 17, 2021

    @jcmoyer
    Contributor

    I ran into a sort of interesting use-case for this. Like many projects, I have a type for a runtime-sized list with a comptime-known maximum length. However, mine is extern (ideally packed, but that has broken codegen at the time of writing) so that I can embed these lists in a larger structure and then use read/writeStruct to serialize the entire thing. This simplifies memory management and has some performance benefits, notably reducing the number of reads/writes that need to be issued.

    Today I saw #9134, which looks great and would ideally replace my own container type, but it returns a standard struct (that is, non-extern, non-packed). So it couldn't be used with read/writeStruct. I set out to see if I could change the layout, and it turns out you can do this today:

    fn makeLayout(comptime T: type, new_layout: std.builtin.TypeInfo.ContainerLayout) type {
        if (@typeInfo(T) != .Struct) {
            @compileError("expected struct, got " ++ @tagName(@typeInfo(T)));
        }
        const struct_info = @typeInfo(T).Struct;
        return @Type(.{ .Struct = .{
            .layout = new_layout,
            .fields = struct_info.fields,
            .decls = struct_info.decls,
            .is_tuple = struct_info.is_tuple,
        } });
    }

    However, I quickly noticed that a couple types I tested this on failed to compile because it triggers the compile errors on deprecated decls. So while this would work initially, if deprecated members were ever added to ArrayListFixed, my code would suddenly fail to compile. Without a way to futureproof against this, it seems that I would be better off maintaining my own implementation.

  6. modified the milestones: 0.9.0, 0.10.0 on Nov 23, 2021
  7. modified the milestones: 0.10.0, 0.11.0 on Apr 16, 2022
  8. InKryption commented on Mar 16, 2023

    @InKryption
    Contributor

    I am interested in seeing at least the @TypeOf(expr) == noreturn and @TypeOf(incompatible, types) == noreturn component of this proposal realized.

    In particular, being able to check whether two types can have peer type resolution performed on them would be very useful for being able to construct more complex type check error messages.

    I have a particular use case, in wanting to modify std.hash_map.verifyContext: currently, it has hard checks, such that for adapted contexts, it will issue a compile error even if the inputted key can coerce to the expected PseudoKey (e.g. *const [n:0]u8, []u8, [:0]const u8, aren't accepted in map.getOrPutAdapted(string_literal, Adapted), where Adapted accepts []const u8 as PseudoKeys).

    One way to solve that problem would be to just remove the checks, but then the resulting error message in the case where the types really aren't compatible, become much less useful than they currently are.
    The second way would be to implement a function in userland that checks whether the types are peer-type-resolvable, but that adds extra maintenance burden.
    The third, and imo simplest way to address this would be to allow @TypeOf given multiple values to return null, or noreturn, or whatever else.

    I have a branch with the basic necessary modifications to Sema for the third option, and I would be happy to go through and update everything else if given the go-ahead.

  9. nektro commented on Mar 16, 2023

    @nektro
    Contributor

    Using .NoReturn for this case doesn't make sense because it represents an expression that causes a runtime exit of the target artifact. whereas .CompileError would mean an expression that halts compilation and cannot be allowed to exist in runtime or comptime.

    one other solution to @TypeOf(incompatible, types) is also to modify it to return ?type instead of modifying std.builtin.Type.

  10. InKryption commented on Mar 16, 2023

    @InKryption
    Contributor

    Making @TypeOf(incompatible, types) == null is actually what I've done in my branch. The bigger question becomes what do do with @TypeOf(val). It would be consistent to have it also return null, but that would then mean all code that uses it and wants to just assume current behavior has to be changed to things like fn foo(a: anytype, b: @TypeOf(a).?) void. It's not the worst, but it does seem odd to require for such a common use case.

  11. nektro commented on Mar 16, 2023

    @nektro
    Contributor

    well for that case in the meantime they would have to add .? but long term im hopeful for #9260 which would change that to fn foo(a: infer A, b: infer B) void or fn foo(a: infer T, b: T) void

  12. InKryption commented on Mar 16, 2023

    @InKryption
    Contributor

    Another idea that just came to mind would be to make it return a specific type - either a primitive (compilationerror), or a declaration in std.builtin (e.g. std.builtin.CompileError) - whose only purpose is to be returned from a @TypeOf call that's given invalid peer types.
    This would allow essentially all code to remain as it is, but adds the ability to do things like @TypeOf(<expr>, ...) == std.builtin.CompileError.

  13. modified the milestones: 0.11.0, 0.12.0 on Apr 9, 2023
  14. modified the milestones: 0.13.0, 0.12.0 on Jul 9, 2023
  15. mnemnion commented on Jun 18, 2024

    @mnemnion

    A minimal fix here might be to add a .type field to Declarations.

    There might be a good reason why this isn't already the case, but if not, this would allow introspection of container types, without taking a field access on them, and triggering a compile error. Whether a decl const oops = @compileError("do not touch!"); should have a special CompileError type, or just NoReturn, seems less important than providing a way to get at that type without a field access.

  16. TheHonestHare commented on Aug 14, 2024

    @TheHonestHare

    It sounds like the @TypeOf builtin would be overloaded too much

    • @TypeOf(single) returns what is says: the type of single, which could be a "compile error" type
    • @TypeOf(multiple, types) returns the peer type of its operands, or returning a "compile error" type if that's impossible, OR returning a "compile error" type if one of the operands would lead to a compile error

    It's fundamentally 2 different behaviours wrapped into 1 builtin. Also, notice how one can't tell the difference between "these 2 types don't have a peer" and "the peer type is a compile error"

    What if a new builtin, @PeerTypeOf(...) ?type (name could be changed), was introduced, that returns the peer type of its operands, or null if that's impossible, or returning a "compile error" type if one of its operands would lead to a compile error.

    Apologies that this is basically reposting #3641, but since this new @TypeOf behaviour of checking for compile error would sort of allow a userland version of @PeerTypeOf anyways I thought it would be relevant.

  17. mnemnion commented on Aug 14, 2024

    @mnemnion

    There was a recent discussion, a short one, on Ziggit, which touches on this issue. Mostly I wanted to point out that the issue this is tracking relates to "bottom types", and the ambiguity and differences of approach to it can be clarified by bringing that concept into focus.

    In sound type systems, Bottom is uninhabited: if type inference derives bottom, then compiling fails. Zig is not interested in building a sound type system, I'm sure, that would make for a very different language. But it's consequential for this discussion, because noreturn is an inhabited bottom type, meaning the actual type of runtime code which compiles just fine. Like it says on the label, you can't return from it, and the documentation explains two consequence of this: it's the realized type of certain expressions, and it also coerces to any other type, so that in the not-uncommon case of two conditional branches, one of which panics, exits, or otherwise won't return a value, the resulting type of that conditional is the one which allows the program to continue. I believe that makes it problematic as the return type of @TypeOf(A,B) for incompatible types, since it implies that for two incompatible types A and B, @TypeOf(@TypeOf(A,B),C)) would be C. But as soon as code is emitted based on that conclusion, that code won't compile.

    An answer of null would improve on the current compile error, but is unsatisfactory in other ways. It would pollute every use of two-argument @TypeOf, and it would mean a separate path for incompatible types: that is correct for runtime code, but I don't see it as ideal for type reflection.

    Nektro raises a good point about the difference between CompileError and NoReturn, namely that CompileError is the comptime 'no return', and NoReturn is the runtime noreturn. I don't see either as suitable for incompatible peer coercion. Since we're facing the difficulty posed by an inhabited bottom type, a good solution is what other type systems where bottom is inhabited do, which is to have an other bottom type, the empty type, which is like noreturn except peer coercion of Empty and T produces Empty, not T. Empty is when you have an inhabited Bottom type and also need an uninhabited one: that's what we're seeing here.

    It doesn't have to have a type-theoretic name like Empty or Bottom, I'd say that Type.NoType is most like the rest of Zig's type system. I do think we need three types here, because NoType isn't even an error, it's informative, telling comptime reflection that it can't emit code which expects the two types to coerce to a common type.

  18. modified the milestones: 0.14.0, 0.15.0 on Feb 9, 2025
  19. mlugg commented on Apr 22, 2026

    @mlugg
    Member

    This use case is better handled by simpler userspace conventions, such as using the value {} or void to represent "unsupported".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions