fix: type-safe stores + Any unbox/eq; finish multi-return deferrals
Type-checking gaps (segfault/corruption → compile errors): - 0197: reject a store into an annotated slot whose value has no modeled coercion AND a different byte width (a 16-byte string into a 4-byte i32 overran the slot and segfaulted). New checkAssignable / noneReinterpretIsUnsafe (coerce.zig, width via the LLVM-accurate typeSizeBytes) wired into every store site: var/const-decl, single + multi assignment (identifier/field/index/ element/deref), named-return defaults. Same-width reinterpretations (*T→[*]T, i64→isize, fn-ref) and explicit xx/cast stay allowed; cascades suppressed via externalErrorsExist. Examples 1205, 1206. - 0198: an implicit `Any → T` unbox is now a compile error (it blindly reinterpreted the boxed payload — silent garbage for a wrong scalar, a segfault for an aggregate). xx and compiler-generated match/pack unboxes are unaffected. Example 1207. - 0199: `Any == <concrete>` (one operand Any) aborted the LLVM verifier — the comparison arm now fires when either operand is Any, boxing the concrete side first. Example 0654. Multi-return deferrals (PLAN-MULTIRET #6 + named-order + D3 + generic): - Reorder named return elements by name instead of requiring slot order; error on unknown/duplicate/missing (value-only AND full-failable-tuple forms). Examples 0210, 0214. - Reject a bare-paren (A, B) multi-return signature in generic-arg position (return-position-only). Example 0215. - Multi-return closure types / lambda literals work via the reused tuple machinery (destructure, single-bind+field, lambda arg). Example 0216. - Generic multi-return: positional works (0217); 0200: the named-slot implicit-return form now works for generic free fns + struct methods — monomorphizeFunction now calls bindNamedReturnSlots. Example 0218. readme.md documents the annotated-store coercion rule; CHECKPOINT-MULTIRET.md updated. Full corpus green (850/0).
This commit is contained in:
@@ -291,13 +291,16 @@ pub fn bindNamedReturnSlots(self: *Lowering, fd: *const ast.FnDecl, ret_ty: Type
|
||||
const dval = self.lowerExpr(dn);
|
||||
self.target_type = saved_target;
|
||||
const dval_ty = self.builder.getRefType(dval);
|
||||
// Reject a default whose type has NO coercion to the slot type (e.g.
|
||||
// `sum: i32 = "hi"`) — a `.none` plan would pass the value through
|
||||
// unchanged and bit-mangle / segfault. (The same hole exists for any
|
||||
// annotated assignment `x: i32 = "hi"` — a broader pre-existing gap.)
|
||||
if (dval_ty != .unresolved and self.coercionResolver().classify(dval_ty, fty) == .none and dval_ty != fty) {
|
||||
// Reject a default whose type has NO coercion to the slot type and a
|
||||
// mismatched byte width (e.g. `sum: i32 = "hi"`) — a `.none` plan
|
||||
// would pass the value through unchanged and overrun / under-fill the
|
||||
// slot, corrupting memory (the same guard as plain annotated
|
||||
// assignment, issue 0197). A same-width `.none` (`p: *void = typed_ptr`)
|
||||
// is a legitimate reinterpretation and stays allowed.
|
||||
if (!self.externalErrorsExist() and dval_ty != .unresolved and self.noneReinterpretIsUnsafe(dval_ty, fty)) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, dn.span, "named return '{s}' has a default of type '{s}' that does not match its declared type '{s}'", .{ nm, self.formatTypeName(dval_ty), self.formatTypeName(fty) });
|
||||
self.assignability_error_count += 1;
|
||||
}
|
||||
self.builder.store(slot, self.buildDefaultValue(fty));
|
||||
} else {
|
||||
@@ -577,6 +580,17 @@ pub fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void {
|
||||
{
|
||||
const ref_ty = self.builder.getRefType(ref);
|
||||
if (ref_ty != ty and ref_ty != .void and ty != .void) {
|
||||
// An initializer with NO coercion to the annotated slot type
|
||||
// (`x : i32 = "hi"`) would otherwise pass through unchanged and
|
||||
// bit-mangle the slot (issue 0197). Diagnose and store a safe
|
||||
// default so the build aborts cleanly instead of segfaulting.
|
||||
if (!self.checkAssignable(ref_ty, ty, val.span, "initialize", vd.name, val)) {
|
||||
self.builder.store(slot, self.buildDefaultValue(ty));
|
||||
if (self.scope) |scope| {
|
||||
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
ref = self.coerceToType(ref, ref_ty, ty);
|
||||
}
|
||||
}
|
||||
@@ -685,6 +699,13 @@ pub fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void {
|
||||
else
|
||||
self.builder.getRefType(ref);
|
||||
|
||||
// An annotated constant whose initializer cannot coerce to the declared type
|
||||
// would be bound under a type its bytes don't match (issue 0197) — diagnose
|
||||
// rather than let a later read reinterpret the wrong-shape value.
|
||||
if (cd.type_annotation != null) {
|
||||
_ = self.checkAssignable(self.builder.getRefType(ref), ty, cd.value.span, "initialize", cd.name, cd.value);
|
||||
}
|
||||
|
||||
if (self.scope) |scope| {
|
||||
scope.put(cd.name, .{ .ref = ref, .ty = ty, .is_alloca = false });
|
||||
}
|
||||
@@ -726,17 +747,10 @@ pub fn validateMultiReturn(self: *Lowering, value_node: *const Node, ret_ty: Typ
|
||||
diags.addFmt(.err, value_node.span, "this function returns {d} values, but {d} {s} given", .{ value_count, els.len, if (els.len == 1) @as([]const u8, "is") else @as([]const u8, "are") });
|
||||
return;
|
||||
}
|
||||
// Named elements must line up with the slots positionally.
|
||||
if (ti.tuple.names) |slot_names| {
|
||||
for (els, 0..) |e, idx| {
|
||||
const en = e.name orelse continue;
|
||||
if (idx >= slot_names.len) continue;
|
||||
const sn = self.module.types.getString(slot_names[idx]);
|
||||
if (sn.len != 0 and !std.mem.eql(u8, en, sn)) {
|
||||
diags.addFmt(.err, value_node.span, "named return element '{s}' does not match the slot '{s}' at position {d} — name the elements in slot order", .{ en, sn, idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Named elements no longer need to be in slot order — `reorderNamedReturn`
|
||||
// (called from `lowerReturn` before lowering) permutes them to match the
|
||||
// slots and diagnoses unknown / duplicate / missing names. Arity is
|
||||
// checked above; nothing more to validate here.
|
||||
} else {
|
||||
// A bare value (not a comma list) where ≥2 are required is valid only if
|
||||
// it already PRODUCES the whole multi-value tuple — forwarding another
|
||||
@@ -751,6 +765,87 @@ pub fn validateMultiReturn(self: *Lowering, value_node: *const Node, ret_ty: Typ
|
||||
}
|
||||
}
|
||||
|
||||
/// Permute a FULLY-NAMED multi-return tuple literal (`return b = …, a = …`) so
|
||||
/// its elements line up with the function's return slots BY NAME, returning a
|
||||
/// fresh reordered `tuple_literal`. Positional / mixed lists, non-tuple returns,
|
||||
/// and arity mismatches (diagnosed in `validateMultiReturn`) pass through
|
||||
/// unchanged. Diagnoses a name that matches no slot, a duplicate, or a missing
|
||||
/// value slot — returning the original node after diagnosing (the build aborts
|
||||
/// via `hasErrors`, so the unpermuted node never reaches run time).
|
||||
fn reorderNamedReturn(self: *Lowering, value_node: *const Node, ret_ty: TypeId) *const Node {
|
||||
if (value_node.data != .tuple_literal) return value_node;
|
||||
if (ret_ty.isBuiltin()) return value_node;
|
||||
const ti = self.module.types.get(ret_ty);
|
||||
if (ti != .tuple) return value_node;
|
||||
const slot_names = ti.tuple.names orelse return value_node;
|
||||
const els = value_node.data.tuple_literal.elements;
|
||||
if (els.len == 0) return value_node;
|
||||
// Reorder only a FULLY-named list; positional/mixed keeps positional order.
|
||||
for (els) |e| if (e.name == null) return value_node;
|
||||
const is_failable = self.errorChannelOf(ret_ty) != null;
|
||||
const fields_len = ti.tuple.fields.len;
|
||||
const value_count = if (is_failable) fields_len - 1 else fields_len;
|
||||
// Two accepted shapes (anything else is an arity error diagnosed by
|
||||
// `validateMultiReturn` — pass through): the VALUE-ONLY list (one element per
|
||||
// value slot, the ergonomic `return a = …, b = …` form) and the FULL-TUPLE
|
||||
// list (a trailing element for the error slot too, `els.len == fields_len`).
|
||||
// BOTH must be reordered/validated — otherwise a fully-named full-tuple
|
||||
// failable return silently lands values positionally (regression found in
|
||||
// review). `match_count` slots participate; the error slot (when present)
|
||||
// joins by its own slot name.
|
||||
const match_count = els.len;
|
||||
if (match_count != value_count and match_count != fields_len) return value_node;
|
||||
if (match_count > slot_names.len) return value_node;
|
||||
|
||||
// Validate element names FIRST (clearer diagnostics than a downstream
|
||||
// "missing slot"): every name must match a participating slot, no duplicates.
|
||||
for (els, 0..) |e, ei| {
|
||||
const en = e.name.?;
|
||||
var matches_slot = false;
|
||||
var s: usize = 0;
|
||||
while (s < match_count) : (s += 1) {
|
||||
const sn = self.module.types.getString(slot_names[s]);
|
||||
if (sn.len != 0 and std.mem.eql(u8, en, sn)) {
|
||||
matches_slot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matches_slot) {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, value_node.span, "named return element '{s}' does not name any return slot", .{en});
|
||||
return value_node;
|
||||
}
|
||||
for (els[ei + 1 ..]) |e2| {
|
||||
if (std.mem.eql(u8, en, e2.name.?)) {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, value_node.span, "named return element '{s}' is given more than once", .{en});
|
||||
return value_node;
|
||||
}
|
||||
}
|
||||
}
|
||||
// All names are distinct participating-slot names and arity matches, so the
|
||||
// mapping is a bijection: every slot has exactly one matching element.
|
||||
const reordered = self.alloc.alloc(ast.TupleElement, match_count) catch return value_node;
|
||||
var slot: usize = 0;
|
||||
while (slot < match_count) : (slot += 1) {
|
||||
const sn = self.module.types.getString(slot_names[slot]);
|
||||
var filled = false;
|
||||
for (els) |e| {
|
||||
if (std.mem.eql(u8, e.name.?, sn)) {
|
||||
reordered[slot] = e;
|
||||
filled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Validation above guarantees a bijection, so every slot is filled. If a
|
||||
// slot is somehow unmatched (e.g. an empty/unnamed slot in a full-tuple
|
||||
// form), bail rather than lower an uninitialized element.
|
||||
if (!filled) return value_node;
|
||||
}
|
||||
|
||||
const node = self.alloc.create(Node) catch return value_node;
|
||||
node.* = .{ .span = value_node.span, .data = .{ .tuple_literal = .{ .elements = reordered } } };
|
||||
return node;
|
||||
}
|
||||
|
||||
pub fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void {
|
||||
if (rs.value) |val| {
|
||||
if (val.data == .identifier and self.isPackName(val.data.identifier.name)) {
|
||||
@@ -789,8 +884,12 @@ pub fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void {
|
||||
// comptime-body return path too (iri.ret_ty is the failable tuple there).
|
||||
const target_for_value = self.failableReturnTarget(ret_ty_for_target, rs.value);
|
||||
if (target_for_value != .void) self.target_type = target_for_value;
|
||||
// Evaluate return value first (before defers)
|
||||
const ret_val = if (rs.value) |val| self.lowerExpr(val) else null;
|
||||
// Evaluate return value first (before defers). A fully-named multi-return
|
||||
// list is permuted to slot order by name (`return b = …, a = …`) before
|
||||
// lowering — `reorderNamedReturn` is a no-op for positional / non-tuple
|
||||
// returns and for the inline-comptime case (ret_ty_for_target carries the
|
||||
// right tuple either way).
|
||||
const ret_val = if (rs.value) |val| self.lowerExpr(reorderNamedReturn(self, val, ret_ty_for_target)) else null;
|
||||
self.target_type = old_target;
|
||||
|
||||
// Inlined-comptime-body return: store into the slot the inliner
|
||||
@@ -1167,6 +1266,10 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
var store_val = val;
|
||||
const val_ty = self.builder.getRefType(val);
|
||||
if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) {
|
||||
// A reassignment with no coercion to the slot type
|
||||
// (`x = "hi"` for `x: i32`) would pass through and
|
||||
// bit-mangle the slot (issue 0197) — diagnose instead.
|
||||
if (!self.checkAssignable(val_ty, binding.ty, asgn.value.span, "reassign", id.name, asgn.value)) return;
|
||||
store_val = self.coerceToType(val, val_ty, binding.ty);
|
||||
}
|
||||
self.builder.store(binding.ref, store_val);
|
||||
@@ -1186,6 +1289,10 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
if (self.resolveGlobalRef(id.name, asgn.target.span)) |gi| {
|
||||
if (asgn.op == .assign) {
|
||||
const val_ty = self.builder.getRefType(val);
|
||||
if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) {
|
||||
// No coercion to the global's type — bit-mangle guard (issue 0197).
|
||||
if (!self.checkAssignable(val_ty, gi.ty, asgn.value.span, "reassign", id.name, asgn.value)) return;
|
||||
}
|
||||
const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void)
|
||||
self.coerceToType(val, val_ty, gi.ty)
|
||||
else
|
||||
@@ -1267,6 +1374,11 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
// *field_ty (the store handler unwraps one pointer level);
|
||||
// fl.ty is the value type to coerce the rhs to.
|
||||
const src_ty = self.builder.getRefType(val);
|
||||
// Guard a width-mismatched `.none` store into the field slot
|
||||
// (`w.s = "hi"` for a struct field `s`) — it would overrun the
|
||||
// slot and corrupt neighbors (issue 0197). Plain `=` only;
|
||||
// compound ops load-op-store through the field type.
|
||||
if (asgn.op == .assign and !self.checkAssignable(src_ty, fl.ty, asgn.value.span, "assign", fa.field, asgn.value)) return;
|
||||
const coerced = self.coerceToType(val, src_ty, fl.ty);
|
||||
self.storeOrCompound(fl.ptr, coerced, asgn.op, fl.ty);
|
||||
} else {
|
||||
@@ -1295,6 +1407,7 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
const fld_ty = tinfo.fields[fi];
|
||||
const base = self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object);
|
||||
const gep = self.builder.structGepTyped(base, fi, self.module.types.ptrTo(fld_ty), obj_ty);
|
||||
if (asgn.op == .assign and !self.checkAssignable(self.builder.getRefType(val), fld_ty, asgn.value.span, "assign", "element", asgn.value)) return;
|
||||
const coerced = self.coerceToType(val, self.builder.getRefType(val), fld_ty);
|
||||
self.storeOrCompound(gep, coerced, asgn.op, fld_ty);
|
||||
return;
|
||||
@@ -1310,6 +1423,10 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
const idx = self.lowerExpr(ie.index);
|
||||
const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty);
|
||||
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
||||
// Guard a width-mismatched `.none` store into an element slot
|
||||
// (`arr[0] = "hi"` for an i32 array) — it would overrun the element
|
||||
// and corrupt neighbors (issue 0197). Plain `=` only.
|
||||
if (asgn.op == .assign and !self.checkAssignable(self.builder.getRefType(val), elem_ty, asgn.value.span, "assign", "element", asgn.value)) return;
|
||||
// For fixed-size array assignment targets, use the alloca pointer directly
|
||||
// so that the store modifies the original variable (not a loaded copy).
|
||||
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
||||
@@ -1342,6 +1459,9 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
break :blk ptr_ty;
|
||||
};
|
||||
const val_ty = self.builder.getRefType(val);
|
||||
// Guard a width-mismatched `.none` store through the pointer
|
||||
// (`p.* = "hi"` for a `*i32`) — overruns the pointee (issue 0197).
|
||||
if (!self.checkAssignable(val_ty, pointee_ty, asgn.value.span, "assign", "target", asgn.value)) return;
|
||||
const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void)
|
||||
self.coerceToType(val, val_ty, pointee_ty)
|
||||
else
|
||||
@@ -1961,6 +2081,8 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||||
if (scope.lookup(id.name)) |binding| {
|
||||
if (binding.is_alloca) {
|
||||
const val_ty = self.builder.getRefType(val);
|
||||
// Width-mismatched `.none` store guard (issue 0197).
|
||||
if (!self.checkAssignable(val_ty, binding.ty, ma.values[i].span, "assign", id.name, ma.values[i])) continue;
|
||||
const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void)
|
||||
self.coerceToType(val, val_ty, binding.ty)
|
||||
else
|
||||
@@ -1986,6 +2108,7 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||||
const base = self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object);
|
||||
const gep = self.builder.structGepTyped(base, fi, self.module.types.ptrTo(fld_ty), obj_ty);
|
||||
const v_ty = self.builder.getRefType(val);
|
||||
if (!self.checkAssignable(v_ty, fld_ty, ma.values[i].span, "assign", "element", ma.values[i])) continue;
|
||||
const sv = if (v_ty != fld_ty and v_ty != .void and fld_ty != .void) self.coerceToType(val, v_ty, fld_ty) else val;
|
||||
self.builder.store(gep, sv);
|
||||
continue;
|
||||
@@ -2005,6 +2128,7 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||||
const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty);
|
||||
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
||||
const val_ty = self.builder.getRefType(val);
|
||||
if (!self.checkAssignable(val_ty, elem_ty, ma.values[i].span, "assign", "element", ma.values[i])) continue;
|
||||
const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void)
|
||||
self.coerceToType(val, val_ty, elem_ty)
|
||||
else
|
||||
@@ -2037,6 +2161,7 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||||
// (or panicked at LLVM emission).
|
||||
if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |r| {
|
||||
const val_ty = self.builder.getRefType(val);
|
||||
if (!self.checkAssignable(val_ty, r.ty, ma.values[i].span, "assign", fa.field, ma.values[i])) continue;
|
||||
const store_val = if (val_ty != r.ty and val_ty != .void and r.ty != .void)
|
||||
self.coerceToType(val, val_ty, r.ty)
|
||||
else
|
||||
@@ -2057,6 +2182,7 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||||
break :blk ptr_ty;
|
||||
};
|
||||
const val_ty = self.builder.getRefType(val);
|
||||
if (!self.checkAssignable(val_ty, pointee_ty, ma.values[i].span, "assign", "target", ma.values[i])) continue;
|
||||
const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void)
|
||||
self.coerceToType(val, val_ty, pointee_ty)
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user