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:
agra
2026-06-27 17:28:27 +03:00
parent 97772abf54
commit b322dcfe61
51 changed files with 1000 additions and 56 deletions

View File

@@ -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