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:
@@ -677,6 +677,29 @@ pub fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mod
|
||||
.optional_wrap => {
|
||||
const child_ty = self.module.types.get(dst_ty).optional.child;
|
||||
const coerced = self.coerceMode(val, src_ty, child_ty, mode);
|
||||
// The inner coercion may classify as `.none` (no built-in applies)
|
||||
// and pass `val` through UNCHANGED — e.g. wrapping a `?i64` value
|
||||
// into a `?(?i64)` whose payload is the 1-tuple `(?i64)`. Building
|
||||
// the optional then inserts a `{i64,i1}` into a `{{i64,i1}}` slot,
|
||||
// producing malformed IR that aborts the LLVM verifier (issue 0165).
|
||||
// If the coerced operand's type does not match the optional's child
|
||||
// type, the wrap is invalid — diagnose loudly instead of emitting a
|
||||
// corrupt InsertValue. (`hasErrors()` then aborts the build.)
|
||||
const coerced_ty = self.builder.getRefType(coerced);
|
||||
if (coerced_ty != child_ty) {
|
||||
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); for any other
|
||||
// mismatch the parens note would be misleading.
|
||||
const note: []const u8 = if (self.module.types.get(child_ty) == .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 wrap a value of type '{s}' into optional '{s}': its payload type is '{s}'{s}", .{ self.formatTypeName(src_ty), self.formatTypeName(dst_ty), self.formatTypeName(child_ty), note });
|
||||
}
|
||||
return val;
|
||||
}
|
||||
return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty);
|
||||
},
|
||||
// Concrete → Protocol (auto type erasure)
|
||||
|
||||
@@ -556,6 +556,23 @@ pub fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 {
|
||||
const inner = self.formatTypeName(v.element);
|
||||
break :blk std.fmt.allocPrint(self.alloc, "Vector({d},{s})", .{ v.length, inner }) catch "vector";
|
||||
},
|
||||
.tuple => |t| blk: {
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
buf.append(self.alloc, '(') catch break :blk "tuple";
|
||||
for (t.fields, 0..) |f, i| {
|
||||
if (i > 0) buf.appendSlice(self.alloc, ", ") catch break :blk "tuple";
|
||||
// Render the field name for named tuples: `(x: i64, y: i64)`.
|
||||
if (t.names) |ns| {
|
||||
if (i < ns.len) {
|
||||
buf.appendSlice(self.alloc, self.module.types.getString(ns[i])) catch break :blk "tuple";
|
||||
buf.appendSlice(self.alloc, ": ") catch break :blk "tuple";
|
||||
}
|
||||
}
|
||||
buf.appendSlice(self.alloc, self.formatTypeName(f)) catch break :blk "tuple";
|
||||
}
|
||||
buf.append(self.alloc, ')') catch break :blk "tuple";
|
||||
break :blk buf.toOwnedSlice(self.alloc) catch "tuple";
|
||||
},
|
||||
else => @tagName(info),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user