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

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

View File

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

View File

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

View File

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

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