Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: remove std.builtin.Type.undefined; the expression undefined requires a result type #22710

Open
mlugg opened this issue Feb 1, 2025 · 1 comment
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@mlugg
Copy link
Member

mlugg commented Feb 1, 2025

RLS has reached a point where it works really nicely as the primary mechanism of type inference; so much so that we have removed anonymous struct types without significant breakage in the wild. The idea here was that anonymous struct type coercions were a legacy feature from before we used result types for type inference.

Zig has two more constructs which obviously fit this bill: null and undefined.

null is unfortunately necessary (at least with how == works today) for x == null to work: since == doesn't forward a result type to either operand, the RHS of == needs to be able to stand without a result type, so null as a standalone expression needs to work. (This could change by changing how == works a little, but that's out of scope for this proposal.)

However, undefined does not match this description. Comparing any value to undefined always results in @as(bool, undefined) (unless the value's type is zero-bit, in which case the comparison is redundant), so it is not a useful construct. In fact, there is no context in which undefined is useful without a result location:

  • a + undefined either invokes Illegal Behavior or returns undefined
  • likewise for all binary operators like +, /, >>, etc
  • likewise for unary operators like -, ~
  • likewise for math-ey builtins like @divExact, @abs, etc
  • passing it to an anytype parameter can only be used by checking for @TypeOf(x) == @Type(.undefined), which is not useful
  • initializing an untyped const with undefined just aliases undefined, which is not useful

Given this, I am proposing the following change to the language:

  • The expression undefined always requires a known result type. It is a compile error if undefined does not have a known result type.
    • So, @TypeOf(undefined) is a compile error.
  • The .undefined variant is removed from std.builtin.Type.

This simplifies the language by removing a type tag, which benefits the language spec (one less type whose behavior must be defined!) and generic Zig code (exhaustively switching on @typeInfo is simpler). It may also make undefined a little easier to understand for beginners, since they'll never have to face the awkward concept of @Type(.undefined); they'll get a useful compile error as soon as they try to misuse the undefined identifier.

Note that under this proposal, undefined retains its status as a builtin identifier which is not a syntactic keyword.

EDIT

I have thought of a realistic use case this will break:

const x = if (something) my_u32 else undefined;

I think code like this ought to have a type annotation for clarity anyway, so if anything, I think this is good, but it is a way in which this proposal is more breaking than I thought.

Note also that #22182 would also break this snippet if accepted.

@mlugg mlugg added breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. labels Feb 1, 2025
@mlugg mlugg modified the milestones: 0.16.0, 0.15.0 Feb 1, 2025
@rohlem
Copy link
Contributor

rohlem commented Feb 1, 2025

I don't think this removes fundamental capabilities, so I guess I'm ambivalent. My thoughts anyway:

  • passing it to an anytype parameter can only be used by checking for @TypeOf(x) == @Type(.undefined), which is not useful

I do think this can be useful in generic API design, however an API can easily expose a userland constant
pub const undefined_value = struct{}{}; and use that as a workaround / different placeholder.
It would be less regular and a bit annoying when you want to signal "this argument won't be used" and have to
decide between undefined and undefined_value based on whether the argument is declared anytype,
but it's not a deal breaker I guess.

If we did want to keep this regularity however, I think we could simply define values of type @TypeOf(undefined)
to lead to a compile error in all contexts except for @TypeOf, assignments, and coercions to other types. (Basically - except in coercions - just like @TypeOf(null).)
(I.e. as operand to any operator, and condition in control flow if,switch,while,for.
I think this is basically the same as status-quo already except for non-branching operators like +%.
I'm guessing they coerce in status-quo, but I'd argue nobody would mind casting the argument/operand there for explicitness.)

This simplifies the language by removing a type tag, which benefits [...] generic Zig code (exhaustively switching on @typeInfo is simpler).

For me personally Zig's regular syntax and semantics are much more helpful to simplicity than ... how many @import("std").builtin.Type states exist.
Not a bad thing to have fewer though.
(We can get rid of the status-quo unimplemented frame and anyframe too btw.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

2 participants