fix: diagnose ?(?T) tuple-payload mismatch instead of malformed IR (issue 0165)

In type position (T) is a 1-tuple (specs.md:843), so ?(?i64) is
optional(tuple(?i64)); assigning a bare ?i64 had coerceToType classify
.none and pass the value through, then optionalWrap built a corrupt
insertvalue that aborted the LLVM verifier. After coercing toward an
optional's child, verify the coerced type equals the child type
(stmt.zig decl-init + coerce.zig .optional_wrap); on mismatch emit a
located diagnostic (tuple-specific note only when the child is a tuple).
formatTypeName now renders tuples as (x: i64, y: i64).

Regressions: optionals/0911 (nested optional via alias, round-trip),
diagnostics/1195 (the mismatch diagnostic). Updated diagnostics/1101 +
protocols/0414 goldens for the improved tuple type-name rendering.
Verified by 3 adversarial reviews. Filed adjacent bug 0171 (?any child
not canonicalized).
This commit is contained in:
agra
2026-06-22 21:54:12 +03:00
parent 3e8d003e3d
commit 0bc8005b99
16 changed files with 198 additions and 4 deletions

View File

@@ -322,6 +322,35 @@ pub fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void {
const child = ty_info.optional.child;
const rt = self.builder.getRefType(ref);
if (rt != child and rt != .void and child != .void) ref = self.coerceToType(ref, rt, child);
// After coercion the value MUST be the optional's payload
// type. If it isn't (the coercion classified `.none` and
// passed the value through unchanged — e.g. a `?i64` value
// flowing into `?(?i64)`, whose payload is the 1-tuple
// `(?i64)`), wrapping anyway inserts a `{i64,i1}` into a
// `{{i64,i1}}` slot and builds malformed IR that aborts the
// LLVM verifier (issue 0165). Diagnose loudly instead.
const post_rt = self.builder.getRefType(ref);
if (post_rt != child and post_rt != .void and child != .void) {
if (self.diagnostics) |d| {
const cs = self.builder.current_span;
// Only mention the `(T)`-is-a-1-tuple gotcha when the
// payload actually IS a tuple (the `?(?T)` typo).
const note: []const u8 = if (self.module.types.get(child) == .tuple)
" (note: in type position '(T)' is a single-field tuple, not a grouping — write the inner optional without parentheses)"
else
"";
d.addFmt(.err, ast.Span{ .start = cs.start, .end = cs.end }, "cannot assign a value of type '{s}' to optional '{s}': its payload type is '{s}'{s}", .{ self.formatTypeName(post_rt), self.formatTypeName(ty), self.formatTypeName(child), note });
}
// Already diagnosed — store the value as-is and bail. The
// trailing coerce below would re-diagnose the same mismatch
// (via the `.optional_wrap` guard in coerce.zig); `hasErrors()`
// aborts the build regardless of the bytes we store.
self.builder.store(slot, ref);
if (self.scope) |scope| {
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
}
return;
}
ref = self.builder.optionalWrap(ref, ty);
} else if (ty_info == .slice) {
// Array → slice promotion: if value is an array, convert to slice