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:
@@ -246,6 +246,13 @@ pub const Lowering = struct {
|
||||
resolved_root: ?*const Node = null, // full AST root (for building comptime modules)
|
||||
comptime_param_nodes: ?std.StringHashMap(*const Node) = null, // active comptime substitutions
|
||||
target_type: ?TypeId = null, // target type for struct/enum literals without explicit names
|
||||
// Count of diagnostics emitted by the annotated-store assignability guard
|
||||
// (`checkAssignable` / the named-return-default guard, issue 0197). Lets the
|
||||
// guard skip when ANY OTHER error already exists (`errorCount() > this`) —
|
||||
// suppressing cascades onto a pre-lowering error (an unknown annotation
|
||||
// type) or a failed initializer, while still reporting multiple INDEPENDENT
|
||||
// mismatches (each of those is one of the guard's OWN errors, not external).
|
||||
assignability_error_count: usize = 0,
|
||||
lowered_functions: std.StringHashMap(void), // tracks which functions have been fully lowered
|
||||
/// Identity map: authoring `*const ast.FnDecl` → the FuncId `declareFunction`
|
||||
/// created for it. The name-keyed function table (`resolveFuncByName`) returns
|
||||
@@ -2044,6 +2051,9 @@ pub const Lowering = struct {
|
||||
pub const lowerCoercedDefault = lower_coerce.lowerCoercedDefault;
|
||||
pub const coerceToType = lower_coerce.coerceToType;
|
||||
pub const coerceExplicit = lower_coerce.coerceExplicit;
|
||||
pub const checkAssignable = lower_coerce.checkAssignable;
|
||||
pub const noneReinterpretIsUnsafe = lower_coerce.noneReinterpretIsUnsafe;
|
||||
pub const externalErrorsExist = lower_coerce.externalErrorsExist;
|
||||
pub const coerceMode = lower_coerce.coerceMode;
|
||||
pub const diagNonIntegralNarrow = lower_coerce.diagNonIntegralNarrow;
|
||||
pub const promoteCVariadicArgs = lower_coerce.promoteCVariadicArgs;
|
||||
|
||||
@@ -603,13 +603,126 @@ pub fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId)
|
||||
return self.coerceMode(val, src_ty, dst_ty, .explicit);
|
||||
}
|
||||
|
||||
/// Is `node` an explicit cast — `xx expr` or `cast(T) expr`? Such a value is
|
||||
/// the user's deliberate opt-in to a reinterpretation that has no standard
|
||||
/// coercion (e.g. pointer↔int, function↔fn-pointer): the `.none` passthrough
|
||||
/// in `coerceMode` is the intended escape hatch there, so the assignability
|
||||
/// guard must NOT fire for it.
|
||||
fn initIsExplicitCast(node: *const Node) bool {
|
||||
return switch (node.data) {
|
||||
.unary_op => |u| u.op == .xx,
|
||||
.call => |c| c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "cast"),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Guard a store into an explicitly-annotated slot against a silent bit-mangle.
|
||||
/// When the initializer/RHS type `src_ty` has NO modeled coercion to the
|
||||
/// destination slot type `dst_ty`, the classifier yields `.none` and
|
||||
/// `coerceMode`'s `.no_op, .none => return val` arm passes the value through
|
||||
/// UNCHANGED — a raw reinterpreting store. That is only DANGEROUS when the
|
||||
/// value's byte width differs from the slot's: a 16-byte `string` written into
|
||||
/// a 4-byte `i32` slot overruns it, corrupting memory and segfaulting at run
|
||||
/// time (issue 0197). A SAME-width `.none` is a bit-compatible reinterpretation
|
||||
/// sx's passthrough has always performed for legitimate pairs that the
|
||||
/// classifier doesn't model — `*T → [*]T`, `i64 → isize`, `*void ← *T`, a bare
|
||||
/// fn-ref into a function slot — so it must stay allowed.
|
||||
///
|
||||
/// Reject ONLY a width mismatch: emit a diagnostic and return false so the
|
||||
/// caller stores a safe default instead of the overrunning value. Returns true
|
||||
/// when the store is sound (a no-op, a modeled conversion, a same-width
|
||||
/// reinterpretation, or a deliberate `xx`/`cast`). `init_node` is the
|
||||
/// initializer expression (null when none); `verb`/`name` shape the message.
|
||||
pub fn checkAssignable(self: *Lowering, src_ty: TypeId, dst_ty: TypeId, span: ast.Span, verb: []const u8, name: []const u8, init_node: ?*const Node) bool {
|
||||
if (src_ty == dst_ty) return true;
|
||||
// Suppress a cascade onto an error that is NOT this guard's own: a
|
||||
// pre-lowering "unknown type" (the annotation resolved to a poison stub) or
|
||||
// a failed initializer leaves an unreliable type here. `errorCount()` minus
|
||||
// the guard's own tally is >0 exactly when some other diagnostic fired — an
|
||||
// errored build never runs, so the bit-mangle can't reach run time anyway.
|
||||
// Independent mismatches in a clean file are each the guard's OWN error, so
|
||||
// they are NOT suppressed (the tally cancels them out).
|
||||
if (self.externalErrorsExist()) return true;
|
||||
// An unresolved operand was already diagnosed at its origin.
|
||||
if (src_ty == .unresolved or dst_ty == .unresolved) return true;
|
||||
if (src_ty == .void or dst_ty == .void) return true;
|
||||
// An explicit `xx`/`cast` is the user opting into a reinterpretation that
|
||||
// has no standard coercion — leave the escape hatch intact, width be damned.
|
||||
if (init_node) |n| if (initIsExplicitCast(n)) return true;
|
||||
if (!self.noneReinterpretIsUnsafe(src_ty, dst_ty)) return true;
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, span, "cannot {s} '{s}' of type '{s}' with a value of type '{s}'", .{ verb, name, self.formatTypeName(dst_ty), self.formatTypeName(src_ty) });
|
||||
self.assignability_error_count += 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// True when a diagnostic OTHER than this guard's own assignability errors has
|
||||
/// already been emitted — the signal to suppress a cascade (see
|
||||
/// `checkAssignable`). The guard tracks its own emissions in
|
||||
/// `assignability_error_count`, so `errorCount() > that` means "an external
|
||||
/// error exists", independent of how many mismatches the guard itself reported.
|
||||
pub fn externalErrorsExist(self: *Lowering) bool {
|
||||
const d = self.diagnostics orelse return false;
|
||||
return d.errorCount() > self.assignability_error_count;
|
||||
}
|
||||
|
||||
/// The core unsafe-store predicate shared by `checkAssignable` and the
|
||||
/// named-return-default guard: a store of `src_ty` into a `dst_ty` slot has NO
|
||||
/// modeled coercion (`coerceMode` would pass it through UNCHANGED) AND the two
|
||||
/// differ in byte width — so the raw store overruns / under-fills the slot,
|
||||
/// corrupting memory (issue 0197). A same-width `.none` is a legitimate
|
||||
/// bit-compatible reinterpretation (`*T → [*]T`, `i64 → isize`, `*void ← *T`),
|
||||
/// which stays allowed. Callers should have already cleared the cheap
|
||||
/// cascade/escape-hatch cases (unresolved operands, explicit `xx`/`cast`).
|
||||
pub fn noneReinterpretIsUnsafe(self: *Lowering, src_ty: TypeId, dst_ty: TypeId) bool {
|
||||
if (src_ty == dst_ty) return false;
|
||||
if (self.coercionResolver().classify(src_ty, dst_ty) != .none) return false;
|
||||
return !sameStoreWidth(self, src_ty, dst_ty);
|
||||
}
|
||||
|
||||
/// ABI/store width of `a` and `b` are equal — the safety test for an unmodeled
|
||||
/// (`.none`) reinterpreting store (see `noneReinterpretIsUnsafe`). Uses
|
||||
/// `typeSizeBytes` (the LLVM-accurate ABI size, with natural field alignment),
|
||||
/// NOT `sizeOf` (which pads every aggregate field to ≥8 and would report
|
||||
/// `struct{i32,i32}` as 16 — coincidentally matching a 16-byte `string` and
|
||||
/// letting the raw store overrun the real 8-byte slot). Comptime-only `pack`
|
||||
/// types have no runtime layout; a pack reaching a store site is a separate,
|
||||
/// already-diagnosed misuse, so treat it as "same width" to avoid a spurious
|
||||
/// second error.
|
||||
fn sameStoreWidth(self: *Lowering, a: TypeId, b: TypeId) bool {
|
||||
if (self.module.types.get(a) == .pack or self.module.types.get(b) == .pack) return true;
|
||||
return self.module.types.typeSizeBytes(a) == self.module.types.typeSizeBytes(b);
|
||||
}
|
||||
|
||||
pub fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mode: CoerceMode) Ref {
|
||||
// PLANNING: classify the built-in coercion (conversions.zig).
|
||||
// EMISSION: each arm below reproduces the original lowering.
|
||||
switch (self.coercionResolver().classify(src_ty, dst_ty)) {
|
||||
.no_op, .none => return val,
|
||||
// Unbox Any → concrete type
|
||||
.unbox_any => return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty),
|
||||
// Unbox Any → concrete type. An IMPLICIT unbox (`s : S = some_any`) is
|
||||
// rejected (issue 0198): the unbox blindly reinterprets the boxed payload
|
||||
// word as `dst_ty` with NO runtime tag check, so a wrong target silently
|
||||
// yields garbage (`f64 = any_holding_i64` → 0.0) or — for an aggregate
|
||||
// target — dereferences the payload word as a pointer and segfaults. sx
|
||||
// prevents this class at compile time (like the no-implicit-optional-unwrap
|
||||
// rule) rather than with a runtime trap: dispatch on the value's type
|
||||
// (`match` / `type_name`), or force it with an explicit `xx` if the boxed
|
||||
// type is known. An EXPLICIT `xx` (mode == .explicit, and `lowerXX`'s own
|
||||
// unbox arm) stays the acknowledged escape hatch; compiler-generated
|
||||
// type-dispatch / pack-extraction unboxes emit `.unbox_any` DIRECTLY (not
|
||||
// through this arm), so they are unaffected.
|
||||
.unbox_any => {
|
||||
if (mode == .implicit) {
|
||||
if (self.diagnostics) |d| {
|
||||
const cs = self.builder.current_span;
|
||||
d.addFmt(.err, ast.Span{ .start = cs.start, .end = cs.end }, "an 'Any' does not implicitly unbox to '{s}': the boxed type is not checked, so a wrong target reinterprets the payload (a wrong scalar silently yields garbage; an aggregate dereferences it and crashes). Dispatch on the value's type with `match`, or force it with `xx` if you know the boxed type.", .{self.formatTypeName(dst_ty)});
|
||||
}
|
||||
// Diagnosed — `hasErrors()` aborts the build before run time; the
|
||||
// emitted op is never executed.
|
||||
}
|
||||
return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty);
|
||||
},
|
||||
// Box concrete → Any
|
||||
.box_any => return self.builder.boxAny(val, src_ty),
|
||||
// Closure VALUE → bare function-pointer slot: not soundly representable.
|
||||
|
||||
@@ -3191,19 +3191,29 @@ pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
||||
}
|
||||
}
|
||||
|
||||
// Any-shaped `==` (e.g. `t == i64` where `t: Type`): both
|
||||
// operands are 16-byte `{tag, value}` aggregates. LLVM
|
||||
// doesn't accept `icmp` on aggregates directly. Decompose
|
||||
// via `unbox_any` (which extracts the value field at
|
||||
// `.i64`) and compare the i64s. Tag fields are stable
|
||||
// across compilations of the same source so value-only
|
||||
// identity is enough.
|
||||
// `Any`-shaped `==` (e.g. `t == i64` where `t: Type`, or `av == 5`): an
|
||||
// `Any` is a 16-byte `{tag, value}` aggregate, which LLVM won't `icmp`
|
||||
// directly. Decompose via `unbox_any` (extracts the value word at `.i64`) and
|
||||
// compare the i64s — tags are stable across a compilation, so value-only
|
||||
// identity is enough. When only ONE operand is `Any` (a MIXED
|
||||
// `Any == <concrete>`), box the concrete side to `Any` first; otherwise it
|
||||
// fell through to a plain `icmp` on the 16-byte aggregate vs a scalar and
|
||||
// aborted the LLVM verifier (issue 0199).
|
||||
if (bop.op == .eq or bop.op == .neq) {
|
||||
const lhs_ty = self.inferExprType(bop.lhs);
|
||||
const rhs_ty = self.inferExprType(bop.rhs);
|
||||
if (lhs_ty == .any and rhs_ty == .any) {
|
||||
const lhs = self.lowerExpr(bop.lhs);
|
||||
const rhs = self.lowerExpr(bop.rhs);
|
||||
const lhs_any = lhs_ty == .any;
|
||||
const rhs_any = rhs_ty == .any;
|
||||
// Need a boxable type on any non-Any side; an already-errored
|
||||
// `.unresolved` / `.void` operand falls through (no spurious cascade).
|
||||
if ((lhs_any or rhs_any) and
|
||||
lhs_ty != .unresolved and rhs_ty != .unresolved and
|
||||
lhs_ty != .void and rhs_ty != .void)
|
||||
{
|
||||
var lhs = self.lowerExpr(bop.lhs);
|
||||
var rhs = self.lowerExpr(bop.rhs);
|
||||
if (!lhs_any) lhs = self.builder.boxAny(lhs, lhs_ty);
|
||||
if (!rhs_any) rhs = self.builder.boxAny(rhs, rhs_ty);
|
||||
const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .i64);
|
||||
const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .i64);
|
||||
if (bop.op == .eq) {
|
||||
|
||||
@@ -152,6 +152,23 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name
|
||||
}
|
||||
}
|
||||
|
||||
// Named multi-return (`-> (x: A, y: B)`): bind the slots as in-scope locals
|
||||
// for the body to assign; `lowerValueBody` then synthesizes the implicit
|
||||
// return from them. The decl path (`lowerFunctionBodyInto`) does this too —
|
||||
// without it a GENERIC named multi-return never sets `named_return_names`, so
|
||||
// the implicit return isn't synthesized and the body wrongly reports
|
||||
// "produces no value" (issue 0200). Save/restore the state so a monomorph
|
||||
// doesn't leak its named-return slots to the enclosing lowering.
|
||||
const saved_nrn_mono = self.named_return_names;
|
||||
const saved_nrd_mono = self.named_return_defaults;
|
||||
self.named_return_names = null;
|
||||
self.named_return_defaults = null;
|
||||
defer {
|
||||
self.named_return_names = saved_nrn_mono;
|
||||
self.named_return_defaults = saved_nrd_mono;
|
||||
}
|
||||
if (fd.abi != .naked) self.bindNamedReturnSlots(fd, ret_ty, &scope);
|
||||
|
||||
// Handle builtin function bodies (e.g. #builtin sqrt monomorphized to sqrt__f32)
|
||||
if (fd.body.data == .builtin_expr) {
|
||||
// Emit builtin call with param 0, then return
|
||||
@@ -396,6 +413,12 @@ pub fn isTypeReturningCallNode(self: *Lowering, node: *const Node) bool {
|
||||
}
|
||||
|
||||
pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
|
||||
// A bare-paren `(A, B)` is a MULTI-RETURN signature — valid only as a
|
||||
// function/closure return type, never as a generic type argument (a
|
||||
// tuple-valued arg uses `Tuple(…)`). Without this it silently resolved to a
|
||||
// reused tuple TypeId (`List((A, B))` ≡ `List(Tuple(A, B))`), eroding the
|
||||
// "multi-return is not a tuple, return-position-only" rule.
|
||||
if (self.rejectMultiReturnValueType(node, "generic type argument")) return .unresolved;
|
||||
// Pack-index access in a type-arg slot (e.g. `type_name($args[0])`
|
||||
// or `type_eq($args[i], i64)`). Same shape as the
|
||||
// `resolveTypeWithBindings` arm — looks up the bound pack types
|
||||
@@ -1820,6 +1843,8 @@ pub fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, ar
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Multi-return signature is return-only, not a type-pack arg.
|
||||
if (self.rejectMultiReturnValueType(a, "generic type argument")) return .unresolved;
|
||||
const ty = self.resolveTypeWithBindings(a);
|
||||
pack_tys.append(self.alloc, ty) catch {};
|
||||
name_parts.appendSlice(self.alloc, "__") catch {};
|
||||
@@ -1832,6 +1857,9 @@ pub fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, ar
|
||||
name_parts.appendSlice(self.alloc, "__") catch {};
|
||||
|
||||
if (tp.is_type_param) {
|
||||
// A bare-paren `(A, B)` multi-return signature is return-position-only,
|
||||
// never a generic type argument (`List((A,B))` — use `Tuple(…)`).
|
||||
if (self.rejectMultiReturnValueType(args[i], "generic type argument")) return .unresolved;
|
||||
const ty = self.resolveTypeWithBindings(args[i]);
|
||||
tb.put(tp.name, ty) catch {};
|
||||
const tname = self.formatTypeName(ty);
|
||||
|
||||
@@ -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