fix(0098): enum literal resolves against the unwrapped optional child; non-enum targets are diagnosed

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.
This commit is contained in:
agra
2026-06-12 12:35:20 +03:00
parent d8076b9333
commit 1bc60d3a35
15 changed files with 253 additions and 1 deletions

View File

@@ -1887,6 +1887,7 @@ pub const Lowering = struct {
pub const lowerTaggedEnumLiteral = lower_expr.lowerTaggedEnumLiteral;
pub const findTaggedVariant = lower_expr.findTaggedVariant;
pub const emitBadVariant = lower_expr.emitBadVariant;
pub const emitBadEnumVariant = lower_expr.emitBadEnumVariant;
pub const resolveVariantValue = lower_expr.resolveVariantValue;
pub const resolveVariantIndex = lower_expr.resolveVariantIndex;
pub const lowerArrayLiteral = lower_expr.lowerArrayLiteral;

View File

@@ -893,11 +893,102 @@ pub fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field:
}
pub fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref {
const target = self.target_type orelse .unresolved;
var target = self.target_type orelse .unresolved;
// An OPTIONAL destination types the literal by its CHILD: `.x` flowing
// into a `?E` slot must produce an `E` for the coercion layer to wrap
// (`.optional_wrap`). Resolving against the optional itself fell into
// resolveVariantValue's non-enum fallback — variant 0, mis-typed as
// the optional (issue 0098).
while (!target.isBuiltin()) {
const info = self.module.types.get(target);
if (info != .optional) break;
target = info.optional.child;
}
const cs = self.builder.current_span;
const span = ast.Span{ .start = cs.start, .end = cs.end };
// The destination must be a known enum / tagged union that carries the
// named variant — every other shape used to lower to a silent 0.
if (target == .unresolved) {
// Cascade guard: an unresolved destination usually means the slot's
// TYPE already failed to resolve and was diagnosed (not-visible /
// ambiguous); a second error on the same line is noise.
if (self.diagnostics) |d| {
if (!d.hasErrors()) {
d.addFmt(.err, span, "enum literal '.{s}' has no destination type to resolve against", .{el.name});
}
}
return self.builder.enumInit(0, Ref.none, target);
}
var known_variant = false;
if (!target.isBuiltin()) {
const info = self.module.types.get(target);
const name_id = self.module.types.internString(el.name);
switch (info) {
.@"enum" => |e| {
for (e.variants) |v| {
if (v == name_id) {
known_variant = true;
break;
}
}
if (!known_variant) self.emitBadEnumVariant(target, e, el.name, span);
},
.tagged_union => |u| {
for (u.fields) |f| {
if (f.name == name_id) {
known_variant = true;
break;
}
}
if (!known_variant) self.emitBadVariant(target, u, el.name, span);
},
else => {},
}
}
if (!known_variant) {
if (self.diagnostics) |d| {
const builtin_or_non_enum = target.isBuiltin() or switch (self.module.types.get(target)) {
.@"enum", .tagged_union => false,
else => true,
};
if (builtin_or_non_enum) {
d.addFmt(.err, span, "enum literal '.{s}' cannot type itself from non-enum destination '{s}'", .{ el.name, self.formatTypeName(target) });
}
}
return self.builder.enumInit(0, Ref.none, target);
}
const tag = self.resolveVariantValue(target, el.name);
return self.builder.enumInit(tag, Ref.none, target);
}
/// The enum twin of `emitBadVariant`: an unknown variant of a plain enum,
/// with the legal variants listed.
pub fn emitBadEnumVariant(
self: *Lowering,
enum_ty: TypeId,
enum_info: types.TypeInfo.EnumInfo,
variant_name: []const u8,
span: ast.Span,
) void {
const diags = self.diagnostics orelse return;
const ty_name = self.formatTypeName(enum_ty);
var list: std.ArrayList(u8) = .empty;
for (enum_info.variants, 0..) |v, i| {
if (i > 0) list.appendSlice(self.alloc, ", ") catch return;
list.appendSlice(self.alloc, self.module.types.getString(v)) catch return;
}
diags.addFmt(
.err,
span,
"'{s}' is not a variant of '{s}' (variants are: {s})",
.{ variant_name, ty_name, list.items },
);
}
/// Lower an `error.X` tag literal to its global tag id (a `u32`). When the
/// destination context (`target_type`) is a named error set, the value is
/// typed as that set and `X`'s membership is validated; otherwise the value