lowerEnumLiteral resolved the variant against the raw destination type,
so any non-enum destination fell into resolveVariantValue's silent
return-0 tail with the enum_init stamped as the wrong type:
- ?E destinations produced variant 0 mis-typed as the optional
(observed as variant 0 OR null, layout-dependent);
- builtin destinations (i64) silently became 0;
- unknown variants of real enums silently became variant 0;
- a destination-less literal panicked LLVM emission (unresolved
type reached codegen).
Now: optional destinations unwrap to the child enum (the coercion
layer's .optional_wrap handles E -> ?E), and the remaining shapes are
diagnosed — unknown variant (with the variant list, via the new
emitBadEnumVariant twin of emitBadVariant), non-enum destination, and
no destination (cascade-guarded: silent when the destination's type
already failed to resolve and was reported).
Regression tests: examples/0183 (return/assign/reassign into ?Enum,
non-zero variants, null path) + examples/1169/1170 (each diagnostic);
all three FAIL on pre-fix master. zig build test 426/426;
tests/run_examples.sh 598/598.
79 lines
3.4 KiB
Markdown
79 lines
3.4 KiB
Markdown
# RESOLVED — 0098: enum literal in a non-enum target silently lowers to variant 0
|
|
|
|
> **RESOLVED** (2026-06-12). Root cause: `lowerEnumLiteral`
|
|
> (src/ir/lower/expr.zig) resolved the variant against the RAW
|
|
> destination type. For any non-enum destination —
|
|
> an OPTIONAL `?E`, a builtin like `i64`, or no destination at all —
|
|
> `resolveVariantValue` fell through its switch to the silent
|
|
> `return 0` tail (the classic silent-fallback-default this repo's
|
|
> CLAUDE.md forbids), and the `enum_init` was stamped with the WRONG
|
|
> type (the optional itself / `.unresolved`). Fix: the literal now
|
|
> unwraps optional destinations and resolves against the CHILD (the
|
|
> coercion layer's `.optional_wrap` then wraps the well-typed `E`
|
|
> into `?E`), and every other shape is DIAGNOSED instead of zeroed:
|
|
> unknown variant of a real enum (with the variant list), non-enum
|
|
> destination, and destination-less literal (cascade-guarded so a
|
|
> destination whose type already failed to resolve doesn't double-
|
|
> report; pre-fix this case PANICKED LLVM emission with an
|
|
> unresolved type). Regression tests:
|
|
> `examples/0183-types-enum-literal-optional-target.sx` (return +
|
|
> assignment + reassignment into `?Enum`, non-zero variants, null
|
|
> path) and `examples/1169/1170-diagnostics-enum-literal-*.sx`
|
|
> (each refusal); all three FAIL on pre-fix master. Gates:
|
|
> `zig build test` 426/426, `tests/run_examples.sh` 598/598.
|
|
|
|
## Symptom
|
|
|
|
An enum LITERAL whose destination is not literally the enum type silently
|
|
lowers to variant 0 (or worse), with no diagnostic.
|
|
|
|
- **Observed**: `return .android_apk;` from a `-> ?Platform` function is
|
|
seen by the caller as `.ios` (variant 0) or even `null`, depending on
|
|
the optional's layout. `x : i64 = .foo;` compiles and `x == 0`.
|
|
`x : Platform = .nonexistent;` compiles to variant 0. `v := .ios;`
|
|
panics LLVM emission ("unresolved type reached LLVM emission").
|
|
- **Expected**: the optional case works (resolve against the child, wrap);
|
|
every unresolvable case is a compile error.
|
|
|
|
Hit in production: /Users/agra/projects/distribution
|
|
`src/server/distd.sx` `ua_platform` (2026-06-12) — every User-Agent
|
|
"detected" as iOS because each `return .<variant>;` into `?Platform`
|
|
lowered to 0. The shipped workaround routed through a typed local
|
|
(`p : Platform = .android_apk; return p;`).
|
|
|
|
## Reproduction
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
Platform :: enum u8 { ios; android_apk; macos; linux; windows; }
|
|
|
|
classify :: (n: i64) -> ?Platform {
|
|
if n == 1 { return .android_apk; } // BUG: caller observes variant 0 / null
|
|
return null;
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
p := classify(1);
|
|
if p == null { return 1; }
|
|
if p! == .android_apk { return 0; }
|
|
return 2;
|
|
}
|
|
```
|
|
|
|
Observed at master d8076b9: exits 1 (null). Expected: exits 0.
|
|
|
|
## Investigation prompt
|
|
|
|
Suspected area: `src/ir/lower/expr.zig` `lowerEnumLiteral` /
|
|
`resolveVariantValue`. The literal resolves against
|
|
`self.target_type` verbatim; an optional target isn't unwrapped, so
|
|
`resolveVariantValue`'s `switch` misses and returns 0, and the
|
|
`enum_init` carries the optional TypeId itself. Fix: unwrap optional
|
|
layers to the child enum before resolving (then the existing
|
|
`.optional_wrap` coercion handles `E` → `?E`), and emit diagnostics
|
|
for unknown variants / non-enum destinations / no destination instead
|
|
of the silent-zero tail. Verification: the repro exits 0; the
|
|
diagnostics cases each error; `zig build test` and
|
|
`tests/run_examples.sh` green; pin the repro as examples.
|