const std = @import("std"); const ast = @import("../../ast.zig"); const Node = ast.Node; const types = @import("../types.zig"); const inst_mod = @import("../inst.zig"); const mod_mod = @import("../module.zig"); const errors = @import("../../errors.zig"); const TypeId = types.TypeId; const Ref = inst_mod.Ref; const Module = mod_mod.Module; const lower = @import("../lower.zig"); const Lowering = lower.Lowering; const Scope = lower.Scope; pub fn lowerBlock(self: *Lowering, node: *const Node) void { switch (node.data) { .block => |blk| { // Create a child scope for block-level variable shadowing var block_scope = Scope.init(self.alloc, self.scope); const saved_scope = self.scope; self.scope = &block_scope; const saved_defer_len = self.defer_stack.items.len; defer { self.emitBlockDefers(saved_defer_len); self.scope = saved_scope; block_scope.deinit(); } for (blk.stmts) |stmt| { if (self.block_terminated) break; self.lowerStmt(stmt); // A bare `return`/`raise` mid-block terminates the current // basic block but deliberately does NOT set `block_terminated` // (that flag would leak past an `if cond { return }` merge // block, skipping its trailing statements — see lowerReturn). // Stop here so dead statements after the terminator aren't // emitted into an already-closed block (invalid LLVM IR). if (self.currentBlockHasTerminator()) break; } }, else => { // Single expression as body (arrow functions) self.lowerStmt(node); }, } } /// Lower an `inline if` branch — block body emits statements, expression returns value. pub fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref { if (node.data == .block) { self.lowerBlock(node); // A `return` inside the branch terminates the current LLVM block; propagate // that up so the enclosing block lowering stops emitting fall-through. if (self.currentBlockHasTerminator()) { self.block_terminated = true; return .none; } return self.builder.constInt(0, .void); } return self.lowerExpr(node); } /// Lower a block and return the last expression's value (for implicit returns). pub fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref { // Set force_block_value so nested if-else expressions produce values const saved = self.force_block_value; self.force_block_value = true; defer self.force_block_value = saved; switch (node.data) { .block => |blk| { if (blk.stmts.len == 0) return null; // Create a child scope for block-level variable shadowing var block_scope = Scope.init(self.alloc, self.scope); const saved_scope = self.scope; self.scope = &block_scope; const saved_defer_len = self.defer_stack.items.len; defer { self.emitBlockDefers(saved_defer_len); self.scope = saved_scope; block_scope.deinit(); } // A block whose last statement is `;`-terminated (or not an // expression) discards its value: lower every statement as a // statement and yield nothing. if (!blk.produces_value) { self.force_block_value = false; for (blk.stmts) |stmt| { if (self.block_terminated) return null; self.lowerStmt(stmt); if (self.currentBlockHasTerminator()) return null; } return null; } // Lower all statements except the last normally self.force_block_value = false; // don't force for non-last statements for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { if (self.block_terminated) return null; self.lowerStmt(stmt); // A bare `return`/`raise` mid-block closes the current basic // block (without setting `block_terminated`); the remaining // statements — including the value-expr — are dead. if (self.currentBlockHasTerminator()) return null; } if (self.block_terminated) return null; // Last statement (no trailing `;`): its value is the block's. self.force_block_value = true; const last = blk.stmts[blk.stmts.len - 1]; return self.tryLowerAsExpr(last); }, else => { // Single expression as body (arrow functions) return self.tryLowerAsExpr(node); }, } } /// Lower a value-returning function body and emit the implicit return. /// Emits a hard error when the body yields no value — its last statement is /// `;`-terminated (value discarded) or void — and the body doesn't already /// terminate via `return`/`raise`. Replaces the old silent default-return. pub fn lowerValueBody(self: *Lowering, body: *const Node, ret_ty: TypeId) void { const body_val = self.lowerBlockValue(body); if (self.currentBlockHasTerminator()) return; if (body_val) |val| { const val_ty = self.builder.getRefType(val); if (val_ty != .void) { const coerced = self.coerceToType(val, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); return; } } // A PURE-failable function (`-> !` / `-> !Named`, whose entire return IS // the error channel) carries no success value — a void body is a normal // success exit, not a missing value. `ensureTerminator` emits the // error-slot-zero success return. if (self.errorChannelOf(ret_ty)) |chan| { if (chan == ret_ty) { self.ensureTerminator(ret_ty); return; } } if (self.diagnostics) |diags| { if (body.data == .block and body.data.block.discarded_semi != null) { diags.addFmt(.err, body.data.block.discarded_semi.?, "function returns '{s}' but the last expression's value is discarded by this `;` — drop the `;` to return it (or use an explicit `return`)", .{self.formatTypeName(ret_ty)}); } else { const span = blk: { if (body.data == .block) { const stmts = body.data.block.stmts; if (stmts.len > 0) break :blk stmts[stmts.len - 1].span; } break :blk body.span; }; diags.addFmt(.err, span, "function returns '{s}' but its body produces no value — end it with a trailing expression (no `;`) or an explicit `return`", .{self.formatTypeName(ret_ty)}); } } self.ensureTerminator(ret_ty); } /// Try to lower a node as an expression, returning its value. /// Statement nodes are lowered as statements (returning null). pub fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref { return switch (node.data) { .var_decl, .const_decl, .fn_decl, .return_stmt, .raise_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => { self.lowerStmt(node); return null; }, else => self.lowerExpr(node), }; } pub fn lowerStmt(self: *Lowering, node: *const Node) void { // Stamp this statement's span onto its instructions (ERR E3.0); see // `lowerExpr`. const saved_span = self.builder.current_span; defer self.builder.current_span = saved_span; if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end }; switch (node.data) { .var_decl => |vd| self.lowerVarDecl(&vd), .const_decl => |cd| self.lowerConstDecl(&cd), // Pointer capture, not by-value: `lowerLocalFnDecl` registers the // decl pointer in `fn_ast_map`, so it must point into the AST node, // not at a stack temporary that the next statement reuses. .fn_decl => |*fd| self.lowerLocalFnDecl(fd), .return_stmt => |rs| self.lowerReturn(&rs), .raise_stmt => |rs| self.lowerRaise(&rs, node.span), .assignment => |asgn| self.lowerAssignment(&asgn), .defer_stmt => |ds| self.lowerDefer(&ds), .onfail_stmt => |ofs| self.lowerOnFail(&ofs, node.span), .push_stmt => |ps| self.lowerPush(&ps), .multi_assign => |ma| self.lowerMultiAssign(&ma), .destructure_decl => |dd| self.lowerDestructureDecl(&dd), .insert_expr => |ins| self.lowerInsertExpr(ins.expr), .block => self.lowerBlock(node), .jni_env_block => |eb| { // Compile-time stack push for lexical-direct env resolution // (2.16b — `#jni_call` in the same fn picks up env from // jni_env_stack directly, no TL read). // // Runtime TL save/set/restore (2.16c) for cross-function // helpers: callees in OTHER fns invoked from inside the // body read the slot via `sx_jni_env_tl_get`. Storage // lives in a separately-linked C helper (see // library/vendors/sx_jni_runtime/sx_jni_env_tl.c) so the // JIT doesn't need orc_rt for TLS. const env_ref = self.lowerExpr(eb.env); const fids = self.getJniEnvTlFids(); const ptr_ty = self.module.types.ptrTo(.void); const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty); const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable; _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void); self.jni_env_stack.append(self.alloc, env_ref) catch unreachable; self.lowerBlock(eb.body); _ = self.jni_env_stack.pop(); const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable; _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void); }, // Block-local type declarations .struct_decl => |sd| { self.recordLocalTypeName(sd.name); self.registerStructDecl(&node.data.struct_decl, node.source_file orelse self.current_source_file); }, .enum_decl => { if (node.data.declName()) |dn| self.recordLocalTypeName(dn); self.registerEnumDecl(&node.data.enum_decl); }, .union_decl => { if (node.data.declName()) |dn| self.recordLocalTypeName(dn); self.registerUnionDecl(&node.data.union_decl); }, .error_set_decl => { if (node.data.declName()) |dn| self.recordLocalTypeName(dn); self.registerErrorSetDecl(node); }, .ufcs_alias => |ua| { self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {}; }, // Expression statement else => { _ = self.lowerExpr(node); }, } } pub fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void { if (vd.value) |val| { if (val.data == .identifier and self.isPackName(val.data.identifier.name)) { const ph = self.diagPackAsValue(val.data.identifier.name, val.span, .storage); // Bind the name to the placeholder so later uses don't cascade // into a second "unresolved" error after this one. if (self.scope) |scope| { scope.put(vd.name, .{ .ref = ph, .ty = .unresolved, .is_alloca = false }); } return; } } if (vd.type_annotation) |ta| { // Explicit type annotation — resolve type first, then lower value const ty = self.resolveType(ta); const slot = self.builder.alloca(ty); if (vd.value) |val| { if (val.data == .undef_literal and !ty.isBuiltin()) { const ti = self.module.types.get(ty); // = --- (undef_literal) on tuple types: zero-initialize if (ti == .tuple) { var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (ti.tuple.fields) |f| { field_vals.append(self.alloc, self.builder.constInt(0, f)) catch unreachable; } const zero = self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); self.builder.store(slot, zero); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } return; } // `---` on an array: explicitly uninitialized — no store. // A whole-array undef store is a store of nothing that // LLVM's legalizer scalarizes into one DAG node per // element (issue 0124: SelectionDAG segfault at ~64K). if (ti == .array) { if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } return; } } // A compile-time float initializer narrowing into an integer // local follows the unified rule (integral folds, non-integral // errors); a runtime float / `xx` cast falls through to the // normal lower+coerce below. if (self.foldComptimeFloatInit(val, ty)) |folded| { self.builder.store(slot, folded); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } return; } const saved_target = self.target_type; const saved_fbv = self.force_block_value; self.target_type = ty; self.force_block_value = true; var ref = self.lowerExpr(val); self.target_type = saved_target; self.force_block_value = saved_fbv; // If target is optional and value isn't null, wrap with optional_wrap if (!ty.isBuiltin()) { const ty_info = self.module.types.get(ty); if (ty_info == .optional and val.data != .null_literal) { ref = self.builder.optionalWrap(ref, ty); } else if (ty_info == .slice) { // Array → slice promotion: if value is an array, convert to slice const ref_ty = self.builder.getRefType(ref); if (!ref_ty.isBuiltin()) { const ref_info = self.module.types.get(ref_ty); if (ref_info == .array) { ref = self.builder.emit(.{ .array_to_slice = .{ .operand = ref } }, ty); } } } else if (self.getProtocolInfo(ty) != null) { // Auto type erasure: concrete → protocol const ref_ty = self.builder.getRefType(ref); if (ref_ty != ty) { ref = self.buildProtocolErasure(ref, val, ref_ty, ty); } } } // Coerce value to match target type (e.g. u8 → i64 widening) { const ref_ty = self.builder.getRefType(ref); if (ref_ty != ty and ref_ty != .void and ty != .void) { ref = self.coerceToType(ref, ref_ty, ty); } } self.builder.store(slot, ref); } else { // No value: zero-initialize or apply struct defaults const zero = self.buildDefaultValue(ty); self.builder.store(slot, zero); } if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } else if (vd.value) |val| { // No type annotation — lower expr first, then get type from result. // This is critical for generic calls where the return type is only // known after monomorphization. const saved_fbv = self.force_block_value; const saved_target = self.target_type; self.force_block_value = true; // An unannotated decl provides no target type: clear the ambient one // (the enclosing fn's implicit-return target) so literal initializers // take their spec defaults (i64/f64) instead of adopting it. self.target_type = null; const ref = self.lowerExpr(val); self.force_block_value = saved_fbv; self.target_type = saved_target; const ty = self.builder.getRefType(ref); const slot = self.builder.alloca(ty); self.builder.store(slot, ref); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } else { const ty = TypeId.i64; const slot = self.builder.alloca(ty); self.builder.store(slot, self.zeroValue(ty)); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } } /// Handle a bare fn_decl node as a local function declaration. /// The parser produces `fn_decl` (not `const_decl`) for `name :: (params) -> T { body }`. pub fn lowerLocalFnDecl(self: *Lowering, fd: *const ast.FnDecl) void { // Use mangled name for local functions to support block-scoped shadowing const name = if (self.scope) |scope| blk: { const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ fd.name, self.local_fn_counter }) catch fd.name; self.local_fn_counter += 1; scope.fn_names.put(fd.name, mangled) catch {}; break :blk mangled; } else fd.name; self.program_index.fn_ast_map.put(name, fd) catch {}; self.lazyLowerFunction(name); } pub fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void { // Handle local function declarations: fx :: (s:i3) -> i3 { ... } if (cd.value.data == .fn_decl) { const fd = &cd.value.data.fn_decl; // Use mangled name for local functions to support block-scoped shadowing const name = if (self.scope != null) blk: { const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ cd.name, self.local_fn_counter }) catch cd.name; self.local_fn_counter += 1; // Register the bare→mangled mapping in the current scope if (self.scope) |scope| { scope.fn_names.put(cd.name, mangled) catch {}; } break :blk mangled; } else cd.name; // Register in fn_ast_map so it can be resolved by lowerCall self.program_index.fn_ast_map.put(name, fd) catch {}; // Lower the function body (saves/restores builder state) self.lazyLowerFunction(name); return; } // Handle local type declarations: MyType :: struct/union/enum { ... } if (cd.value.data == .struct_decl) { self.recordLocalTypeName(cd.name); self.registerStructDecl(&cd.value.data.struct_decl, self.current_source_file); return; } if (cd.value.data == .enum_decl) { self.recordLocalTypeName(cd.name); self.registerEnumDecl(&cd.value.data.enum_decl); return; } if (cd.value.data == .union_decl) { self.recordLocalTypeName(cd.name); self.registerUnionDecl(&cd.value.data.union_decl); return; } const ref = self.lowerExpr(cd.value); // If there's an explicit type annotation, use it. Otherwise, infer from the expression. const ty = if (cd.type_annotation) |ta| self.resolveType(ta) else self.builder.getRefType(ref); if (self.scope) |scope| { scope.put(cd.name, .{ .ref = ref, .ty = ty, .is_alloca = false }); } } 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)) { _ = self.diagPackAsValue(val.data.identifier.name, val.span, .return_value); return; } } // Set target_type to function return type so null_literal etc. get the right type. // When inlining a comptime body, the *inlined* fn's declared return type wins // over the caller's — otherwise `return 42` inside a `-> i64` body lowered into // a `-> i32` caller would coerce 42 to i32 before storing into the i64 slot. const old_target = self.target_type; const ret_ty_for_target: TypeId = if (self.inline_return_target) |iri| iri.ret_ty else if (self.builder.func) |fid| self.module.functions.items[@intFromEnum(fid)].ret else TypeId.i64; // A value-carrying failable (`-> (T..., !)`) returns its VALUE part and // the success error slot (0) is appended by lowerFailableSuccessReturn. // Resolve a BARE returned value against that value type, NOT the failable // tuple: a bare enum literal `.variant` resolves its tag against // `target_type`, and against the tuple it matches no variant (tag 0) and // is stamped with the tuple type — which the success-return path then // mistakes for a forwarded full tuple, dropping the appended `0` slot. // An explicit full failable tuple return (`return (v..., e)`) keeps the // full-tuple target so its trailing error element resolves against the // error set; it is then forwarded as-is. Applies to the inlined // 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; self.target_type = old_target; // Inlined-comptime-body return: store into the slot the inliner // gave us and branch to the inliner's "return-done" basic block. // The branch is the basic block's terminator — so subsequent // dead code in the same block trips the LLVM verifier (the // SAME behaviour as a regular `return X;` followed by code). // // We DO NOT set `block_terminated = true`: that flag would // leak past structured control flow (e.g. an `if cond { return // X; }` whose merge block continues to subsequent statements) // and incorrectly skip the trailing statements. CFG-level // termination is what we actually want — let the basic-block // terminator do its job. if (self.inline_return_target) |iri| { if (ret_val) |ref| { // Value-carrying failable inlined body: append the success error // slot (0) exactly like the real-return path below. // lowerFailableSuccessReturn routes through emitTupleRet, which // stores into iri.slot and branches to iri.done_bb for an inline // target. Defers first, so the returned SSA value is materialized // before they run (matching the real-return ordering). if (!iri.ret_ty.isBuiltin() and self.module.types.get(iri.ret_ty) == .tuple and self.errorChannelOf(iri.ret_ty) != null) { self.emitBlockDefers(self.func_defer_base); self.lowerFailableSuccessReturn(ref, iri.ret_ty, rs.value.?.span); return; } const val_ty = self.builder.getRefType(ref); const coerced = if (val_ty != iri.ret_ty) self.coerceToType(ref, val_ty, iri.ret_ty) else ref; self.builder.store(iri.slot, coerced); } // Drain block-scoped defers up to the inlined-body base so // they fire on this return path the same as a real fn return. self.emitBlockDefers(self.func_defer_base); self.builder.br(iri.done_bb, &.{}); return; } // Emit ALL pending defers for THIS function in LIFO order before the return self.emitBlockDefers(self.func_defer_base); if (ret_val) |ref| { const ret_ty = if (self.builder.func) |fid| self.module.functions.items[@intFromEnum(fid)].ret else TypeId.i64; if (ret_ty == .void) { // Void function — just return void (the value expression was evaluated for side effects) self.builder.retVoid(); } else if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) { // Value-carrying failable `-> (T..., !)`: the user returns the // value part; the compiler appends the success error slot (0). self.lowerFailableSuccessReturn(ref, ret_ty, rs.value.?.span); } else { // Coerce return value to match function return type (e.g., ?i32 → i32) const val_ty = self.builder.getRefType(ref); const coerced = self.coerceToType(ref, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); } } else { // A bare `return;` in a pure failable function (`-> !` / `-> !Named`, // whose return type IS the error set) is the success exit — the // error slot carries 0 ("no error"). Everything else is a void return. const ret_ty = if (self.builder.func) |fid| self.module.functions.items[@intFromEnum(fid)].ret else TypeId.void; if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .error_set) { self.builder.ret(self.builder.constInt(0, ret_ty), ret_ty); } else { self.builder.retVoid(); } } } /// The ROOT identifier of an assignment-target chain (`K[0].x` → "K", /// `WHITE.r` → "WHITE"). A deref along the chain (`p.*`, `p.*[i]`) breaks /// it — writing through a pointer VALUE is not a write to the named root. fn assignmentRootIdent(target: *const Node) ?[]const u8 { var n = target; while (true) { switch (n.data) { .identifier => |id| return id.name, .index_expr => |ie| n = ie.object, .field_access => |fa| n = fa.object, else => return null, } } } /// True when `root` names a module CONSTANT from the current source — /// a const-flagged global (array/struct consts, #run consts) or a /// module value const. Locals shadow (caller checks scope first). fn rootIsConstant(self: *Lowering, root: []const u8) bool { switch (self.selectGlobalAuthor(root)) { .resolved => |g| if (self.module.globals.items[g.id.index()].is_const) return true, else => {}, } return switch (self.selectModuleConst(root)) { .resolved, .own_opaque => true, .ambiguous, .none => false, }; } pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { // Writes through a constant are rejected at compile time (issue 0116): // the target chain's root naming a const global (array/struct consts, // #run consts) or a module value const cannot be stored to — for a // struct const the store previously compiled and bus-errored at // runtime; for scalars it silently misfired. if (assignmentRootIdent(asgn.target)) |root| { const shadowed = if (self.scope) |s| s.lookup(root) != null else false; if (!shadowed and rootIsConstant(self, root)) { if (self.diagnostics) |d| d.addFmt(.err, asgn.target.span, "cannot assign through constant '{s}' — constants are immutable (use a '=' global or a local copy for mutable data)", .{root}); return; } } // Set target_type from LHS for RHS lowering (enum literals, struct literals, etc.) const old_target = self.target_type; if (asgn.target.data == .identifier) { var found_local = false; if (self.scope) |scope| { if (scope.lookup(asgn.target.data.identifier.name)) |binding| { self.target_type = binding.ty; found_local = true; } } if (!found_local) { // Quiet author-aware lookup (type inference only; the store // site diagnoses ambiguity / visibility). if (self.program_index.global_names.get(asgn.target.data.identifier.name)) |gi| { switch (self.selectGlobalAuthor(asgn.target.data.identifier.name)) { .resolved => |g| self.target_type = g.ty, .untracked => self.target_type = gi.ty, else => {}, } } } } else if (asgn.target.data == .index_expr) { // For array[i] = val, set target_type to the element type const tgt_obj_ty = self.inferExprType(asgn.target.data.index_expr.object); const elem_ty = self.ptrToArrayElem(tgt_obj_ty) orelse self.getElementType(tgt_obj_ty); if (elem_ty != .void) self.target_type = elem_ty; } else if (asgn.target.data == .field_access) { // For obj.field = val, set target_type to the field's type so RHS // sub-expressions (enum/struct literals, branch arms, xx casts) can // resolve against it. Skipped for forms that would forward the type // unchanged into method-call arg slots (`resolveCallParamTypes` can't // override target_type per-arg). const needs_target = switch (asgn.value.data) { .enum_literal, .struct_literal, .tuple_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true, .call => |vc| vc.callee.data == .enum_literal, else => false, }; if (needs_target) { const fa = asgn.target.data.field_access; const obj_ty_raw = self.inferExprType(fa.object); const obj_ty = if (!obj_ty_raw.isBuiltin()) blk: { const pinfo = self.module.types.get(obj_ty_raw); break :blk if (pinfo == .pointer) pinfo.pointer.pointee else obj_ty_raw; } else obj_ty_raw; // Resolve the LHS member's type via the SAME resolver the lvalue- // pointer path uses (fieldLvalueResolve), so the RHS target type // and the store slot can't diverge. Covers union/tagged-union // direct + promoted members, tuple/vector lanes, and structs — // not just structs (a plain getStructFields loop returned nothing // for a union member, leaving a struct-literal RHS untyped → // struct_init.ty == .unresolved → LLVM-emission panic; issue 0133). if (self.fieldLvalueResolve(obj_ty, fa.field)) |res| { self.target_type = res.valueType(); } } } const val = self.lowerExpr(asgn.value); self.target_type = old_target; switch (asgn.target.data) { .identifier => |id| { var handled = false; if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { handled = true; if (asgn.op == .assign) { // Coerce value to match binding type (e.g., f32 → ?f32, concrete → protocol) var store_val = val; const val_ty = self.builder.getRefType(val); if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) { store_val = self.coerceToType(val, val_ty, binding.ty); } self.builder.store(binding.ref, store_val); } else { // Compound assignment: load, op, store const loaded = self.builder.load(binding.ref, binding.ty); const result = self.emitCompoundOp(loaded, val, asgn.op, binding.ty); self.builder.store(binding.ref, result); } } } } // Fallback: global variable assignment — source-aware (issue // 0115): write the AUTHOR's global, never an unrelated module's // same-named one. if (!handled) { if (self.resolveGlobalRef(id.name, asgn.target.span)) |gi| { if (asgn.op == .assign) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) self.coerceToType(val, val_ty, gi.ty) else val; self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void); } else { // Compound assignment: load current value, apply op, store back const loaded = self.builder.emit(.{ .global_get = gi.id }, gi.ty); const result = self.emitCompoundOp(loaded, val, asgn.op, gi.ty); self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = result } }, .void); } } } }, .field_access => |fa| { // M2.2 — `obj.field = val` for an Obj-C `#property` field // dispatches via objc_msgSend `setField:`. Skip struct- // pointer / GEP entirely; receivers are opaque Obj-C ids. // Compound ops on properties are deferred (need load-via- // getter + op + store-via-setter — Month 4 ARC territory). if (asgn.op == .assign) { if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { self.lowerObjcPropertySetter(fa.object, prop, val); return; } } // M1.2 A.3 — `self.field [op]= val` on a sx-defined Obj-C // class instance field (NOT a #property): write through // the __sx_state ivar. Handles plain assignment AND // compound ops (+=, -=, etc.) via storeOrCompound. if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { const obj_ref = self.lowerExpr(fa.object); const state_ptr = self.lowerObjcDefinedStateForObj(obj_ref, info.fcd) orelse return; const ptr_void = self.module.types.ptrTo(.void); const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = info.field_idx, .base_type = info.state_ty, } }, ptr_void); self.storeOrCompound(field_addr, val, asgn.op, info.field_ty); return; } var obj_ptr = self.lowerExprAsPtr(fa.object); var obj_ty = self.inferExprType(fa.object); // Auto-deref: if the object is a pointer field from a non-identifier // (i.e., result of structGep on a pointer slot), load the pointer value. if (fa.object.data != .identifier and !obj_ty.isBuiltin()) { const pinfo = self.module.types.get(obj_ty); if (pinfo == .pointer) { obj_ptr = self.builder.load(obj_ptr, obj_ty); obj_ty = pinfo.pointer.pointee; } } // Reject a direct write to a tagged-union variant (issue 0136): it // sets the payload but not the tag. Construct via `x = .variant(...)`. if (self.diagTaggedUnionVariantWrite(obj_ty, fa.field, asgn.target.span)) return; // Special .len/.ptr handling only for slices, strings, arrays — NOT structs const is_special_container = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { const obj_info = self.module.types.get(obj_ty); break :blk obj_info == .slice or obj_info == .array or obj_info == .vector; } else false); if (is_special_container and std.mem.eql(u8, fa.field, "len")) { const gep = self.builder.structGepTyped(obj_ptr, 1, .i64, obj_ty); self.storeOrCompound(gep, val, asgn.op, .i64); } else if (is_special_container and std.mem.eql(u8, fa.field, "ptr")) { const gep = self.builder.structGepTyped(obj_ptr, 0, .i64, obj_ty); self.storeOrCompound(gep, val, asgn.op, .i64); } else if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |fl| { // Resolve the target field (struct / union direct / promoted // anonymous-struct member / tuple element / vector lane) via // the shared lvalue resolver — the same one the address-of // and multi-target store paths use — so the three never // resolve a field to a different slot or default field 0 // (two-resolver defect class). fl.ptr is // *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); const coerced = self.coerceToType(val, src_ty, fl.ty); self.storeOrCompound(fl.ptr, coerced, asgn.op, fl.ty); } else { // No struct / union / tuple / vector field matches the // assignment target. Emit the same field-not-found // diagnostic the read path uses (emitFieldError) and bail; // building a pointer with field_ty = .unresolved would // otherwise store through a pointer-to-.unresolved that // panics at LLVM emission. _ = self.emitFieldError(obj_ty, fa.field, asgn.target.span); } }, .index_expr => |ie| { const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); // 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; const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null; if (obj_alloca) |alloca_ref| { // Array alloca: single-index GEP with element stride const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } else if (is_array) { // Array in a struct field or other composite: get pointer to array in-place const obj_ptr = self.lowerExprAsPtr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj_ptr, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } else { // Pointer/slice: load the pointer value and GEP const obj = self.lowerExpr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } }, .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); if (asgn.op == .assign) { const pointee_ty = blk: { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk ptr_ty; }; const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) self.coerceToType(val, val_ty, pointee_ty) else val; self.builder.store(ptr, store_val); } else { const pointee_ty = self.inferExprType(de.operand); const elem_ty = blk: { if (!pointee_ty.isBuiltin()) { const info = self.module.types.get(pointee_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk pointee_ty; }; self.storeOrCompound(ptr, val, asgn.op, elem_ty); } }, else => { _ = self.emitError("assignment_target", asgn.target.span); }, } } const FieldLvalue = struct { ptr: Ref, ty: TypeId }; /// Pure description of which slot `obj.field` resolves to — the GEP path plus /// the field's value type — computed WITHOUT emitting any IR. The single /// field-matching resolver for the LVALUE/WRITE paths: `fieldLvaluePtr` builds /// GEPs from it, and the assignment target-type path reads `.valueType()` from /// it, so the lvalue-pointer path and the RHS target-type path can never /// disagree on which field (or what type) a name resolves to — the two-resolver /// defect class this codebase keeps burning on. To handle a new aggregate /// shape, add an arm here and a matching GEP arm in `fieldLvaluePtr`; both fail /// to compile until the union is exhaustive, forcing the two to stay in lockstep. /// /// NOTE: the READ path (`lowerFieldAccess`, expr.zig) and the TYPE-INFER path /// (`ExprTyper.inferType`, expr_typer.zig) still carry their OWN parallel field /// matchers (emitting `union_get`/`enum_payload`/`struct_get` value reads, and /// returning a bare `TypeId`, respectively). They are not yet routed through /// here, so a new aggregate shape must currently be taught to all three. Folding /// read + infer onto this resolver (switching the descriptor to value-read ops / /// `.valueType()`) would make it the genuine compiler-wide single matcher. const FieldResolution = union(enum) { /// Direct union/tagged-union member: union_gep(index) into the aggregate. union_direct: struct { index: u32, ty: TypeId }, /// Promoted member of an anonymous-struct union variant: union_gep into /// the variant struct `variant_ty`, then struct_gep into the member. union_promoted: struct { variant_index: u32, variant_ty: TypeId, member_index: u32, ty: TypeId }, /// Tuple element / vector lane / plain struct field: a single /// struct_gep(index) into the aggregate. indexed: struct { index: u32, ty: TypeId }, /// The field's value type — what the caller coerces the rhs to / sets as /// the RHS target type. Identical regardless of the GEP path taken. fn valueType(self: FieldResolution) TypeId { return switch (self) { .union_direct => |u| u.ty, .union_promoted => |u| u.ty, .indexed => |s| s.ty, }; } }; /// Match `obj.field` against the aggregate `obj_ty` and return the resolution /// descriptor, or null when no field matches (the caller emits the /// field-not-found diagnostic). Emits NO IR — see `FieldResolution`. /// /// Handles union direct fields, promoted anonymous-struct union members, /// tuple elements (numeric or named), vector lanes (`.x`/`.y`/`.z`/`.w` and /// the colour aliases), and plain struct fields. pub fn fieldLvalueResolve(self: *Lowering, obj_ty: TypeId, field: []const u8) ?FieldResolution { if (obj_ty.isBuiltin()) return null; const field_name_id = self.module.types.internString(field); const type_info = self.module.types.get(obj_ty); // Union / tagged-union: variants overlay at offset 0. A direct field is a // union_gep; a promoted anonymous-struct member is a union_gep into the // variant followed by a struct_gep into the member. const union_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (type_info) { .@"union" => |u| u.fields, .tagged_union => |u| u.fields, else => null, }; if (union_fields) |fields| { for (fields, 0..) |f, i| { if (f.name == field_name_id) { return .{ .union_direct = .{ .index = @intCast(i), .ty = f.ty } }; } if (!f.ty.isBuiltin()) { const fi = self.module.types.get(f.ty); if (fi == .@"struct") { for (fi.@"struct".fields, 0..) |sf, si| { if (sf.name == field_name_id) { return .{ .union_promoted = .{ .variant_index = @intCast(i), .variant_ty = f.ty, .member_index = @intCast(si), .ty = sf.ty } }; } } } } } return null; } // Tuple element: `.0` (numeric) or `.name`. if (type_info == .tuple) { const tup = type_info.tuple; var elem_idx: ?usize = null; if (std.fmt.parseInt(usize, field, 10)) |n| { if (n < tup.fields.len) elem_idx = n; } else |_| { if (tup.names) |names| { for (names, 0..) |nm, i| { if (nm == field_name_id and i < tup.fields.len) { elem_idx = i; break; } } } } if (elem_idx) |idx| { return .{ .indexed = .{ .index = @intCast(idx), .ty = tup.fields[idx] } }; } return null; } // Vector lane: `.x`/`.y`/`.z`/`.w` (or colour aliases `.r`/`.g`/`.b`/`.a`) // → lane 0/1/2/3 via the same vectorLaneIndex the read path uses. A // non-lane field on a vector is a genuine miss (caller diagnoses). if (type_info == .vector) { const vidx = Lowering.vectorLaneIndex(field) orelse return null; return .{ .indexed = .{ .index = vidx, .ty = type_info.vector.element } }; } // Plain struct field. const struct_fields = self.getStructFields(obj_ty); for (struct_fields, 0..) |f, i| { if (f.name == field_name_id) { return .{ .indexed = .{ .index = @intCast(i), .ty = f.ty } }; } } return null; } /// Resolve `obj.field` — where `obj_ptr` already points at the aggregate — /// to a typed pointer into the field's storage plus the field's value type. /// Delegates the field MATCH to `fieldLvalueResolve` (shared with the RHS /// target-type path) and only builds the GEP(s) here. Returns null when no /// field matches; the caller emits the field-not-found diagnostic. /// /// `ptr`'s IR type is `*field_ty` (a pointer to the field), NOT the field /// value type: `emitStore` reads the store-target pointer's IR type and /// unwraps one `.pointer` level to find the stored value's type. Labelling /// the GEP with the bare field type instead would make a field whose own /// type is a pointer-to-aggregate (`*Pair`) coerce the stored pointer into /// the aggregate (closure auto-promotion in `coerceArg`), storing an /// oversized struct that clobbers the neighbouring field. `.ty` carries the /// field's value type for the caller's coercion. /// /// Single source of lvalue field GEP-building shared by all three store/ /// address-of sites — lowerAssignment (single-target store), lowerExprAsPtr /// (address-of), and lowerMultiAssign (multi-target store); the field MATCH /// itself is delegated to `fieldLvalueResolve` (above), so they never resolve /// a field to a different slot or default field 0. pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []const u8) ?FieldLvalue { const res = self.fieldLvalueResolve(obj_ty, field) orelse return null; switch (res) { .union_direct => |u| { const ptr = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = u.index, .base_type = obj_ty } }, self.module.types.ptrTo(u.ty)); return .{ .ptr = ptr, .ty = u.ty }; }, .union_promoted => |u| { const ug = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = u.variant_index, .base_type = obj_ty } }, self.module.types.ptrTo(u.variant_ty)); const ptr = self.builder.structGepTyped(ug, u.member_index, self.module.types.ptrTo(u.ty), u.variant_ty); return .{ .ptr = ptr, .ty = u.ty }; }, .indexed => |s| { const ptr = self.builder.structGepTyped(obj_ptr, s.index, self.module.types.ptrTo(s.ty), obj_ty); return .{ .ptr = ptr, .ty = s.ty }; }, } } /// True (and emits the diagnostic) when `obj.field` names a DIRECT variant of a /// tagged union — a store target that would set the payload but NOT the tag /// (issue 0136): a tagged union is laid out `{ tag, payload }`, the write path /// emits a `union_gep` into the payload only, so the discriminant goes stale and /// a later `match`/`==` takes the wrong arm. The variant is set via construction /// (`x = .variant(...)`, which writes both), so a direct member write is rejected. /// /// Returns false (keeps working) for: plain `union` (no tag); promoted / nested /// sub-field writes (`s.rect.w = ...`, where the immediate object is the payload /// struct, resolving to `.indexed`/`.union_promoted`, not `.union_direct`); and /// non-aggregates. Derefs one pointer level so a `*TaggedUnion` receiver is /// caught too. Uses the shared `fieldLvalueResolve` matcher, so the guard can't /// drift from the store path's notion of which member a name resolves to. pub fn diagTaggedUnionVariantWrite(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) bool { var ty = obj_ty; if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .pointer) ty = info.pointer.pointee; } if (ty.isBuiltin() or self.module.types.get(ty) != .tagged_union) return false; const res = self.fieldLvalueResolve(ty, field) orelse return false; if (res != .union_direct) return false; if (self.diagnostics) |d| d.addFmt(.err, span, "cannot assign to tagged-union variant '{s}' directly — a member write sets the payload but leaves the tag stale; construct the variant instead (e.g. `x = .{s}(...)`)", .{ field, field }); return true; } /// Get the pointer (alloca ref) for an lvalue expression, without loading. pub fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref { switch (node.data) { .identifier => |id| { const local = if (self.scope) |scope| scope.lookup(id.name) else null; if (local) |binding| { if (binding.is_alloca) { // If the variable IS a pointer (e.g., p: *Vec2), load it // to get the actual pointer value for GEP/store operations if (!binding.ty.isBuiltin()) { const info = self.module.types.get(binding.ty); if (info == .pointer) { return self.builder.load(binding.ref, binding.ty); } } return binding.ref; } } else if (self.resolveGlobalRef(id.name, null)) |gi| { // Module-global lvalue: address into the global's live storage // so a downstream GEP/store targets the global itself, not a // loaded copy. A pointer-typed global is loaded first to get // the pointer value to GEP through (mirrors the local pointer // case above); any other global yields its storage address. if (!gi.ty.isBuiltin() and self.module.types.get(gi.ty) == .pointer) { return self.builder.emit(.{ .global_get = gi.id }, gi.ty); } return self.builder.emit(.{ .global_addr = gi.id }, self.module.types.ptrTo(gi.ty)); } }, .field_access => |fa| { var obj_ptr = self.lowerExprAsPtr(fa.object); var obj_ty = self.inferExprType(fa.object); // Auto-deref for chained pointer field access: // When fa.object is a field_access or index_expr, lowerExprAsPtr returns // a structGep/pointer to the slot. If the slot holds a pointer type, // we need to load the pointer value before GEPing into the pointee struct. // (Identifiers are already loaded by the identifier handler in lowerExprAsPtr.) if (fa.object.data != .identifier and !obj_ty.isBuiltin()) { const info = self.module.types.get(obj_ty); if (info == .pointer) { obj_ptr = self.builder.load(obj_ptr, obj_ty); obj_ty = info.pointer.pointee; } } // Resolve the field lvalue (struct / union direct / promoted // anonymous-struct member / tuple element) via the shared // resolver so address-of and the multi-target store path never // disagree on the slot. No match → emit the read path's // field-not-found diagnostic (lowerFieldAccessOnType → // emitFieldError) instead of silently GEPing field 0 as .i64; // that bogus pointer reaches LLVM emission as ptrTo(.unresolved) // and panics. if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |r| return r.ptr; return self.emitFieldError(obj_ty, fa.field, node.span); }, .index_expr => |ie| { const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); // For fixed-size arrays, use the alloca so GEP addresses the original memory const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object)) else self.lowerExpr(ie.object); return self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); }, .deref_expr => |de| { return self.lowerExpr(de.operand); }, else => {}, } // Fallback: lower as expression (may produce a value, not pointer) return self.lowerExpr(node); } /// Store a value to a GEP, handling both plain and compound assignment. pub fn storeOrCompound(self: *Lowering, gep: Ref, val: Ref, op: ast.Assignment.Op, ty: TypeId) void { if (op == .assign) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != ty and val_ty != .void and ty != .void) self.coerceToType(val, val_ty, ty) else val; self.builder.store(gep, store_val); } else { const loaded = self.builder.load(gep, ty); const result = self.emitCompoundOp(loaded, val, op, ty); self.builder.store(gep, result); } } pub fn emitCompoundOp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.Assignment.Op, ty: TypeId) Ref { return switch (op) { .add_assign => self.builder.add(lhs, rhs, ty), .sub_assign => self.builder.sub(lhs, rhs, ty), .mul_assign => self.builder.mul(lhs, rhs, ty), .div_assign => self.builder.div(lhs, rhs, ty), .mod_assign => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty), .and_assign => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty), .or_assign => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty), .xor_assign => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), .shl_assign => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), .shr_assign => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), else => self.emitError("compound_assign", null), }; } // ── Defer / cleanup ───────────────────────────────────────────── pub fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void { // Push deferred expression onto the stack — emitted at every block exit, LIFO. self.defer_stack.append(self.alloc, .{ .body = ds.expr, .is_onfail = false }) catch {}; } /// `onfail [e] BODY` (ERR E1.7) — cleanup that runs only when an error /// leaves the enclosing block. Recorded on the shared cleanup stack; /// emitted (interleaved with defers, reverse) at error exits by /// `emitErrorCleanup`, and discarded — never run — on a success exit. pub fn lowerOnFail(self: *Lowering, ofs: *const ast.OnFailStmt, span: ast.Span) void { // `onfail` is only meaningful inside a failable function — a // non-failable function never error-exits, so it could never fire. const ret_ty = self.effectiveReturnType() orelse { self.diagOnFailNotFailable(span); return; }; if (self.errorChannelOf(ret_ty) == null) { self.diagOnFailNotFailable(span); return; } self.defer_stack.append(self.alloc, .{ .body = ofs.body, .is_onfail = true, .binding = ofs.binding }) catch {}; } pub fn diagOnFailNotFailable(self: *Lowering, span: ast.Span) void { if (self.diagnostics) |diags| { diags.addFmt(.err, span, "`onfail` is only valid inside a failable function (a return type with `!` or `!Named`) — use `defer` for unconditional cleanup", .{}); } } /// Emit cleanups from saved_len..current in reverse (LIFO) order on a /// SUCCESS exit: only `defer` entries run; `onfail` entries are skipped /// (and discarded by the truncation). Truncates the stack to saved_len. pub fn emitBlockDefers(self: *Lowering, saved_len: usize) void { // Guard: if stack was already drained (e.g., by a return that emitted all defers) if (saved_len > self.defer_stack.items.len) return; if (self.currentBlockHasTerminator()) { // Block already terminated (e.g., by return) — cleanups were already emitted self.defer_stack.shrinkRetainingCapacity(saved_len); return; } const stack = self.defer_stack.items; var i = stack.len; while (i > saved_len) { i -= 1; if (!stack[i].is_onfail) self.lowerCleanupBody(stack[i].body); } self.defer_stack.shrinkRetainingCapacity(saved_len); } /// Emit pending `defer` cleanups for a `break`/`continue` exit: everything /// registered since the innermost loop's body began, in LIFO order. `onfail` /// entries are skipped (a break is a success exit). The stack is NOT /// truncated — the same entries still belong to the fall-through lowering /// path after the branch that contains the break; the enclosing block scopes /// truncate as usual. pub fn emitLoopExitDefers(self: *Lowering) void { const stack = self.defer_stack.items; var i = stack.len; while (i > self.loop_defer_base) { i -= 1; if (!stack[i].is_onfail) self.lowerCleanupBody(stack[i].body); } } /// Run a `defer`/`onfail` cleanup body for its side effects (void context). /// A braced body lowers as statements (NOT as a value) so a trailing-`;` /// last expression is fine here — cleanup bodies never yield a value. pub fn lowerCleanupBody(self: *Lowering, body: *const Node) void { if (body.data == .block) self.lowerBlock(body) else _ = self.lowerExpr(body); } /// Emit cleanups from `base`..current in reverse order on an ERROR exit /// (raise / try-propagation): BOTH `defer` and `onfail` entries run, /// interleaved in reverse declaration order. `err_tag` is the in-flight /// error tag, bound to each `onfail e`'s binding. Does not truncate — the /// terminating `ret` + the unwinding block-scope `emitBlockDefers` (which /// then see the terminator and skip) leave the stack consistent. pub fn emitErrorCleanup(self: *Lowering, base: usize, err_tag: Ref) void { if (base > self.defer_stack.items.len) return; const tag_ty = self.builder.getRefType(err_tag); const stack = self.defer_stack.items; var i = stack.len; while (i > base) { i -= 1; const entry = stack[i]; if (entry.is_onfail) { if (entry.binding) |name| { var ofscope = Scope.init(self.alloc, self.scope); const saved = self.scope; self.scope = &ofscope; ofscope.put(name, .{ .ref = err_tag, .ty = tag_ty, .is_alloca = false }); self.lowerCleanupBody(entry.body); self.scope = saved; ofscope.deinit(); } else { self.lowerCleanupBody(entry.body); } } else { self.lowerCleanupBody(entry.body); } } } pub fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void { // push Context.{...} { body } — allocates a fresh Context on the // stack frame, rebinds the lowering's `current_ctx_ref` to it for // the body's lexical scope, then restores. No global, no walk. if (!self.implicit_ctx_enabled) { _ = self.diagnoseMissingContext("`push Context.{...}`"); self.lowerBlock(ps.body); return; } const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { _ = self.diagnoseMissingContext("`push Context.{...}`"); self.lowerBlock(ps.body); return; }; const saved_ctx_ref = self.current_ctx_ref; defer self.current_ctx_ref = saved_ctx_ref; const slot = self.builder.alloca(ctx_ty); // Inherit-omitted semantics: a `push Context.{ ... }` is a CAPABILITY // bag — fields the literal does NOT name are inherited from the ambient // context, not zero-inited. Zero-init would install a NULL `io`/ // `allocator` vtable (a latent crash if the field is later used inside // the pushed scope). So seed the new slot from the ambient context, // then overwrite only the fields the literal explicitly names. // // This applies only to a `Context.{...}` struct-literal context-expr; // any other form (e.g. `push some_ctx_value`) keeps the whole-value // store (no field-level merge to do). const lit: ?*const ast.StructLiteral = switch (ps.context_expr.data) { .struct_literal => |*sl| sl, else => null, }; if (lit != null and self.current_ctx_ref != Ref.none) { // 1. Copy the ambient context into the fresh slot (load + store the // whole struct), so every omitted field carries its current value. const ambient = self.builder.load(self.current_ctx_ref, ctx_ty); self.builder.store(slot, ambient); // 2. Overwrite only the named fields. `push Context.{...}` always // uses named field-inits (it is a Context literal); a positional // init has no field name to target, so it is rejected loudly // rather than silently writing the wrong field. self.current_ctx_ref = slot; // body + field values see the new slot for (lit.?.field_inits) |fi| { const fname = fi.name orelse { if (self.diagnostics) |d| d.addFmt(.err, ps.context_expr.span, "`push Context.{{...}}` requires named fields (positional init not supported)", .{}); continue; }; const fl = self.fieldLvaluePtr(slot, ctx_ty, fname) orelse { _ = self.emitFieldError(ctx_ty, fname, ps.context_expr.span); continue; }; const saved_target_f = self.target_type; self.target_type = fl.ty; const fval = self.lowerExpr(fi.value); self.target_type = saved_target_f; const fval_ty = self.builder.getRefType(fval); const store_val = if (fval_ty != fl.ty and fval_ty != .void and fl.ty != .void) self.coerceToType(fval, fval_ty, fl.ty) else fval; self.builder.store(fl.ptr, store_val); } } else { // Non-literal context-expr, or no ambient context to inherit from: // lower the whole value and store it (the original behaviour). const saved_target = self.target_type; self.target_type = ctx_ty; const ctx_val = self.lowerExpr(ps.context_expr); self.target_type = saved_target; self.builder.store(slot, ctx_val); self.current_ctx_ref = slot; } self.lowerBlock(ps.body); } pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void { // Evaluate all RHS values first, then assign to LHS targets var vals = std.ArrayList(Ref).empty; defer vals.deinit(self.alloc); for (ma.values) |v| { vals.append(self.alloc, self.lowerExpr(v)) catch unreachable; } for (ma.targets, 0..) |target, i| { if (i >= vals.items.len) break; const val = vals.items[i]; switch (target.data) { .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) self.coerceToType(val, val_ty, binding.ty) else val; self.builder.store(binding.ref, store_val); } } } }, .index_expr => |ie| { const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); 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); const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void) self.coerceToType(val, val_ty, elem_ty) else val; // For fixed-size arrays, use the alloca pointer directly const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null; if (obj_alloca) |alloca_ref| { const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty); self.builder.store(gep, store_val); } else { const obj = self.lowerExpr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty); self.builder.store(gep, store_val); } }, .field_access => |fa| { const obj_ptr = self.lowerExprAsPtr(fa.object); const obj_ty = self.inferExprType(fa.object); // Reject a direct write to a tagged-union variant (issue 0136). if (self.diagTaggedUnionVariantWrite(obj_ty, fa.field, target.span)) continue; // Resolve the target field via the shared lvalue resolver — // the same one address-of uses — so a missing field emits a // diagnostic instead of defaulting to field 0 / field_ty // .unresolved, which silently corrupted a neighbouring field // (or panicked at LLVM emission). if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |r| { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != r.ty and val_ty != .void and r.ty != .void) self.coerceToType(val, val_ty, r.ty) else val; self.builder.store(r.ptr, store_val); } else { _ = self.emitFieldError(obj_ty, fa.field, target.span); } }, .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); const pointee_ty = blk: { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk ptr_ty; }; const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) self.coerceToType(val, val_ty, pointee_ty) else val; self.builder.store(ptr, store_val); }, else => { _ = self.emitError("multi_assign_target", target.span); }, } } } pub fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void { // Lower the RHS expression (must produce a tuple) const saved_fbv = self.force_block_value; const saved_target = self.target_type; self.force_block_value = true; // Same as the unannotated var-decl path: the destructure declares new // bindings, so the ambient target type must not type the RHS literals. self.target_type = null; const ref = self.lowerExpr(dd.value); self.force_block_value = saved_fbv; self.target_type = saved_target; const ty = self.builder.getRefType(ref); // Get tuple field info if (ty.isBuiltin()) return; const ti = self.module.types.get(ty); if (ti != .tuple) return; const tuple = ti.tuple; if (dd.names.len > tuple.fields.len) return; // E1.8 (discard rejection): when the RHS is a value-carrying failable, // the error slot (always the LAST tuple field) cannot be dropped. It is // dropped when the destructure omits it (fewer names than fields, so the // trailing error slot is never reached) or binds it to `_`. The `try` / // `catch` / `or value` consumer forms all strip the error channel (their // result type is non-failable), so this fires only on a BARE failable // destructure — exactly the case that would let an error vanish silently. if (self.errorChannelOf(ty) != null) { const err_dropped = dd.names.len < tuple.fields.len or std.mem.eql(u8, dd.names[dd.names.len - 1], "_"); if (err_dropped) { if (self.diagnostics) |diags| { diags.addFmt(.err, dd.value.span, "the error slot of a failable cannot be dropped — bind it (`v, err := …`) and handle it, or use `try` / `catch`", .{}); } } } // Extract each field and bind to a new variable for (dd.names, 0..) |name, i| { if (std.mem.eql(u8, name, "_")) continue; // discard const field_ty = tuple.fields[i]; const field_val = self.builder.emit(.{ .tuple_get = .{ .base = ref, .field_index = @intCast(i), .base_type = ty, } }, field_ty); const slot = self.builder.alloca(field_ty); self.builder.store(slot, field_val); if (self.scope) |scope| { scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true }); } } // Destructuring a failable's result binds the error slot to a variable: // the user now owns the error explicitly, so the trace is absorbed // (ERR E3.2). A plain (non-failable) tuple destructure clears nothing. if (self.errorChannelOf(ty) != null) self.emitTraceClear(); }