From 3dbd3ce07280da50bbe91175960a9cf696295cdc Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 13:14:51 +0300 Subject: [PATCH] refactor(B3.1): move statement lowering to lower/stmt.zig Verbatim relocation of the 24-method statement cluster (block/stmt dispatch, var/const/local-fn decls, return, assignment + compound ops, multi-assign/destructure, push, defer/onfail/cleanup) plus the nested single-home FieldLvalue type into src/ir/lower/stmt.zig. 24 aliases on Lowering keep all call sites unchanged. Method pub-flips: buildDefaultValue, buildProtocolErasure, diagPackAsValue, diagnoseMissingContext, emitError, emitFieldError, ensureTerminator, getExprAlloca, getJniEnvTlFids, isPackName, lazyLowerFunction, lowerObjcDefinedStateForObj, lowerObjcPropertySetter, recordLocalTypeName, registerEnumDecl, registerErrorSetDecl, registerStructDecl, registerUnionDecl, zeroValue. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn. --- src/ir/lower.zig | 1293 ++--------------------------------------- src/ir/lower/stmt.zig | 1276 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1322 insertions(+), 1247 deletions(-) create mode 100644 src/ir/lower/stmt.zig diff --git a/src/ir/lower.zig b/src/ir/lower.zig index ff23364..91f590d 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -35,6 +35,7 @@ const ObjcLowering = @import("ffi_objc.zig").ObjcLowering; const semantic_diagnostics = @import("semantic_diagnostics.zig"); const lower_error = @import("lower/error.zig"); const lower_comptime = @import("lower/comptime.zig"); +const lower_stmt = @import("lower/stmt.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -2059,7 +2060,7 @@ pub const Lowering = struct { /// mistakes it for a namespaced-only leak (see `local_type_names`). Keyed by the /// declaring source (the function being lowered) so the local is visible only /// within that source. - fn recordLocalTypeName(self: *Lowering, name: []const u8) void { + pub fn recordLocalTypeName(self: *Lowering, name: []const u8) void { const src = self.current_source_file orelse self.main_file orelse return; const gop = self.local_type_names.getOrPut(src) catch return; if (!gop.found_existing) gop.value_ptr.* = std.StringHashMap(void).init(std.heap.page_allocator); @@ -2434,7 +2435,7 @@ pub const Lowering = struct { /// Lazily lower a function body on demand. Called when lowerCall can't find /// the function and it exists in fn_ast_map. - fn lazyLowerFunction(self: *Lowering, name: []const u8) void { + pub fn lazyLowerFunction(self: *Lowering, name: []const u8) void { // Already lowered? if (self.lowered_functions.contains(name)) return; @@ -2752,970 +2753,6 @@ pub const Lowering = struct { // ── Statement lowering ────────────────────────────────────────── - 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. - 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. - 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), - .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); - }, - } - } - - 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| { - // = --- (undef_literal) on tuple types: zero-initialize - if (val.data == .undef_literal and !ty.isBuiltin()) { - const ti = self.module.types.get(ty); - 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; - } - } - // 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 → s64 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; - self.force_block_value = true; - const ref = self.lowerExpr(val); - self.force_block_value = saved_fbv; - 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.s64; - 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 }`. - 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); - } - - fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void { - // Handle local function declarations: fx :: (s:s3) -> s3 { ... } - 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 }); - } - } - - 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 `-> s64` body lowered into - // a `-> s32` caller would coerce 42 to s32 before storing into the s64 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.s64; - // 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.s64; - 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., ?s32 → s32) - 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(); - } - } - } - - fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { - // 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) { - if (self.program_index.global_names.get(asgn.target.data.identifier.name)) |gi| { - self.target_type = gi.ty; - } - } - } else if (asgn.target.data == .index_expr) { - // For array[i] = val, set target_type to the element type - const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object)); - 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; - if (!obj_ty.isBuiltin()) { - const field_name_id = self.module.types.internString(fa.field); - const struct_fields = self.getStructFields(obj_ty); - for (struct_fields) |f| { - if (f.name == field_name_id) { - self.target_type = f.ty; - break; - } - } - } - } - } - 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 - if (!handled) { - if (self.program_index.global_names.get(id.name)) |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; - } - } - - // 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, .s64, obj_ty); - self.storeOrCompound(gep, val, asgn.op, .s64); - } else if (is_special_container and std.mem.eql(u8, fa.field, "ptr")) { - const gep = self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty); - self.storeOrCompound(gep, val, asgn.op, .s64); - } 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 - // (issue 0094 / issue-0083 two-resolver 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 (issue 0094). - _ = 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.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 }; - - /// 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. - /// 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. 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 resolution shared by all three store/ - /// address-of sites — lowerAssignment (single-target store), lowerExprAsPtr - /// (address-of), and lowerMultiAssign (multi-target store) — so they never - /// resolve a field to a different slot or default field 0 (issue 0094). - fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []const u8) ?FieldLvalue { - 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) { - const ptr = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); - return .{ .ptr = ptr, .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) { - const ug = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); - const ptr = self.builder.structGepTyped(ug, @intCast(si), self.module.types.ptrTo(sf.ty), f.ty); - return .{ .ptr = ptr, .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| { - const elem_ty = tup.fields[idx]; - const ptr = self.builder.structGepTyped(obj_ptr, @intCast(idx), self.module.types.ptrTo(elem_ty), obj_ty); - return .{ .ptr = ptr, .ty = elem_ty }; - } - 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; - const elem_ty = type_info.vector.element; - const ptr = self.builder.structGepTyped(obj_ptr, vidx, self.module.types.ptrTo(elem_ty), obj_ty); - return .{ .ptr = ptr, .ty = elem_ty }; - } - - // Plain struct field. - const struct_fields = self.getStructFields(obj_ty); - for (struct_fields, 0..) |f, i| { - if (f.name == field_name_id) { - const ptr = self.builder.structGepTyped(obj_ptr, @intCast(i), self.module.types.ptrTo(f.ty), obj_ty); - return .{ .ptr = ptr, .ty = f.ty }; - } - } - return null; - } - - /// Get the pointer (alloca ref) for an lvalue expression, without loading. - 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.program_index.global_names.get(id.name)) |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 .s64; - // that bogus pointer reaches LLVM emission as ptrTo(.unresolved) - // and panics (issue 0094). - 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.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. - 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); - } - } - - 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), - }; - } - - // ── Expression lowering ───────────────────────────────────────── - pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // Stamp this node's source span onto the instructions it emits (ERR // E3.0 — feeds DWARF line-info + comptime frame resolution). Save/ @@ -6842,7 +5879,7 @@ pub const Lowering = struct { /// `xs` is a pack name used where a runtime value is required. A pack is /// comptime-only (Decision 1), so this is an error — with a context-tailored /// suggestion for how to express the intent instead. - fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: PackValueKind) Ref { + pub fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: PackValueKind) Ref { if (self.diagnostics) |d| { const id = d.addFmtId(.err, span, "pack '{s}' has no runtime value — a pack is comptime-only and can't be used as a value here", .{name}); switch (kind) { @@ -6857,7 +5894,7 @@ pub const Lowering = struct { } /// True when `name` is a pack parameter bound in the current mono body. - fn isPackName(self: *Lowering, name: []const u8) bool { + pub fn isPackName(self: *Lowering, name: []const u8) bool { const ppc = self.pack_param_count orelse return false; return ppc.contains(name); } @@ -8903,7 +7940,7 @@ pub const Lowering = struct { /// the program hasn't registered the type — i.e. doesn't transitively /// import `modules/std.sx`. Returns a placeholder Ref so the lowering /// can keep going and surface any additional errors. - fn diagnoseMissingContext(self: *Lowering, what: []const u8) Ref { + pub fn diagnoseMissingContext(self: *Lowering, what: []const u8) Ref { if (self.diagnostics) |d| { const span = ast.Span{ .start = 0, .end = 0 }; d.addFmt(.err, span, "{s} requires the Context type — add `#import \"modules/std.sx\";` (or a module that imports it)", .{what}); @@ -9749,270 +8786,6 @@ pub const Lowering = struct { // ── Defer/Push/MultiAssign ────────────────────────────────────── - 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. - 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 {}; - } - - 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. - 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); - } - - /// 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. - 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); - } - } - } - - 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 saved_target = self.target_type; - self.target_type = ctx_ty; - const ctx_val = self.lowerExpr(ps.context_expr); - self.target_type = saved_target; - - const slot = self.builder.alloca(ctx_ty); - self.builder.store(slot, ctx_val); - self.current_ctx_ref = slot; - - self.lowerBlock(ps.body); - } - - 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.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); - // 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) (issue 0094). - 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); - }, - } - } - } - - fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void { - // Lower the RHS expression (must produce a tuple) - const saved_fbv = self.force_block_value; - self.force_block_value = true; - const ref = self.lowerExpr(dd.value); - self.force_block_value = saved_fbv; - 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(); - } - - // ── Comptime lowering ──────────────────────────────────────────── - /// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value}, /// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name. pub fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void { @@ -14049,7 +12822,7 @@ pub const Lowering = struct { /// Register a `Foo :: error { A, B }` declaration as an error-set type. /// Rejects an empty set here (sema gate) since type_bridge has no /// diagnostics; non-empty sets are interned via type_bridge. - fn registerErrorSetDecl(self: *Lowering, node: *const Node) void { + pub fn registerErrorSetDecl(self: *Lowering, node: *const Node) void { const esd = node.data.error_set_decl; if (esd.tag_names.len == 0) { if (self.diagnostics) |diags| { @@ -14516,7 +13289,7 @@ pub const Lowering = struct { } } - fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void { + pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void { const table = &self.module.types; const name_id = table.internString(sd.name); @@ -14668,7 +13441,7 @@ pub const Lowering = struct { /// the shared `type_bridge.buildEnumInfo`; `internNamedTypeDecl` interns it under /// the computed nominal id and records `decl_key → TypeId` so `namedRefTid` /// resolves bare references to this exact author. - fn registerEnumDecl(self: *Lowering, ed: *const ast.EnumDecl) void { + pub fn registerEnumDecl(self: *Lowering, ed: *const ast.EnumDecl) void { const table = &self.module.types; const name_id = table.internString(ed.name); const decl_key: *const anyopaque = @ptrCast(ed); @@ -14679,7 +13452,7 @@ pub const Lowering = struct { /// Register a top-level UNION decl under a per-decl nominal identity (E6a) — /// the union twin of `registerEnumDecl` / `registerStructDecl`. - fn registerUnionDecl(self: *Lowering, ud: *const ast.UnionDecl) void { + pub fn registerUnionDecl(self: *Lowering, ud: *const ast.UnionDecl) void { const table = &self.module.types; const name_id = table.internString(ud.name); const decl_key: *const anyopaque = @ptrCast(ud); @@ -15112,7 +13885,7 @@ pub const Lowering = struct { /// orc_rt platform support. AOT targets get the same .c file /// linked in via `needs_jni_env_tl_runtime`, which Compilation /// reads to append a synthetic c_import alongside the user's. - fn getJniEnvTlFids(self: *Lowering) struct { get: FuncId, set: FuncId } { + pub fn getJniEnvTlFids(self: *Lowering) struct { get: FuncId, set: FuncId } { self.needs_jni_env_tl_runtime = true; const ptr_ty = self.module.types.ptrTo(.void); if (self.jni_env_tl_get_fid == null) { @@ -15957,7 +14730,7 @@ pub const Lowering = struct { return self.coerceToType(val, src, dst); } - fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref { + pub fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref { const dst_info = self.module.types.get(dst_ty); if (dst_info != .@"struct") return operand; const proto_name = self.module.types.getString(dst_info.@"struct".name); @@ -16083,7 +14856,7 @@ pub const Lowering = struct { /// Produce a default value for a type, applying struct field defaults. /// For structs with defaults (e.g., `b: s32 = 99`), creates a struct_literal with defaults applied. /// For other types, returns a zero value. - fn buildDefaultValue(self: *Lowering, ty: TypeId) Ref { + pub fn buildDefaultValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); if (info != .@"struct" and info != .tuple) return self.zeroValue(ty); @@ -16132,7 +14905,7 @@ pub const Lowering = struct { /// Produce a zero/default value for any type — constInt(0) for integers, /// constNull for pointers, constUndef for structs/complex types. - fn zeroValue(self: *Lowering, ty: TypeId) Ref { + pub fn zeroValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); return switch (info) { @@ -16233,7 +15006,7 @@ pub const Lowering = struct { if (self.current_source_file) |src| node.source_file = src; } - fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref { + pub fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref { if (self.diagnostics) |diags| { // The literal message carries the lowering's `current_source_file` // and enclosing function name. The diagnostic renderer's @@ -16259,7 +15032,7 @@ pub const Lowering = struct { return self.emitPlaceholder(name); } - fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { + pub fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { // A field access on an already-`.unresolved` object is a cascade from an // upstream type-resolution failure that was ALREADY diagnosed (e.g. an // unresolvable / oversized array dimension — issue 0083). The @@ -16427,7 +15200,7 @@ pub const Lowering = struct { /// Get the alloca Ref for an expression, if it's a simple variable reference. /// Returns null for complex expressions (field access, function calls, etc.) - fn getExprAlloca(self: *Lowering, node: *const Node) ?Ref { + pub fn getExprAlloca(self: *Lowering, node: *const Node) ?Ref { const name = switch (node.data) { .identifier => |id| id.name, .type_expr => |te| te.name, @@ -16661,7 +15434,7 @@ pub const Lowering = struct { } } - fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void { + pub fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void { if (self.currentBlockHasTerminator()) return; if (ret_ty == .noreturn) { // A `-> noreturn` function never returns; if control reaches the @@ -16855,7 +15628,7 @@ pub const Lowering = struct { /// `state = object_getIvar(obj, load(___state_ivar))`. Shared /// helper for state-field read + write (M1.2 A.3). - fn lowerObjcDefinedStateForObj(self: *Lowering, obj_ref: Ref, fcd: *const ast.ForeignClassDecl) ?Ref { + pub fn lowerObjcDefinedStateForObj(self: *Lowering, obj_ref: Ref, fcd: *const ast.ForeignClassDecl) ?Ref { const ptr_void = self.module.types.ptrTo(.void); const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null; defer self.alloc.free(ivar_global_name); @@ -16893,7 +15666,7 @@ pub const Lowering = struct { /// `objc_msg_send(obj, sel_set:, val)`. M2.2 — setter side. /// Selector: prepend "set", capitalize the first letter of the /// field name, append ":". `backgroundColor` → `setBackgroundColor:`. - fn lowerObjcPropertySetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.ForeignFieldDecl, val: Ref) void { + pub fn lowerObjcPropertySetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.ForeignFieldDecl, val: Ref) void { const obj_ref = self.lowerExpr(obj_expr); const vptr_ty = self.module.types.ptrTo(.void); @@ -18182,6 +16955,32 @@ pub const Lowering = struct { pub const sourceModuleConst = lower_comptime.sourceModuleConst; pub const pinConstAuthorSource = lower_comptime.pinConstAuthorSource; pub const foldComptimeFloatInit = lower_comptime.foldComptimeFloatInit; + + // --- moved to lower/stmt.zig (lower_stmt) --- + pub const lowerBlock = lower_stmt.lowerBlock; + pub const lowerInlineBranch = lower_stmt.lowerInlineBranch; + pub const lowerBlockValue = lower_stmt.lowerBlockValue; + pub const lowerValueBody = lower_stmt.lowerValueBody; + pub const tryLowerAsExpr = lower_stmt.tryLowerAsExpr; + pub const lowerStmt = lower_stmt.lowerStmt; + pub const lowerVarDecl = lower_stmt.lowerVarDecl; + pub const lowerLocalFnDecl = lower_stmt.lowerLocalFnDecl; + pub const lowerConstDecl = lower_stmt.lowerConstDecl; + pub const lowerReturn = lower_stmt.lowerReturn; + pub const lowerAssignment = lower_stmt.lowerAssignment; + pub const fieldLvaluePtr = lower_stmt.fieldLvaluePtr; + pub const lowerExprAsPtr = lower_stmt.lowerExprAsPtr; + pub const storeOrCompound = lower_stmt.storeOrCompound; + pub const emitCompoundOp = lower_stmt.emitCompoundOp; + pub const lowerMultiAssign = lower_stmt.lowerMultiAssign; + pub const lowerDestructureDecl = lower_stmt.lowerDestructureDecl; + pub const lowerPush = lower_stmt.lowerPush; + pub const lowerDefer = lower_stmt.lowerDefer; + pub const lowerOnFail = lower_stmt.lowerOnFail; + pub const diagOnFailNotFailable = lower_stmt.diagOnFailNotFailable; + pub const emitBlockDefers = lower_stmt.emitBlockDefers; + pub const lowerCleanupBody = lower_stmt.lowerCleanupBody; + pub const emitErrorCleanup = lower_stmt.emitErrorCleanup; }; /// JNI param/return type resolution: user-declared types pass through diff --git a/src/ir/lower/stmt.zig b/src/ir/lower/stmt.zig new file mode 100644 index 0000000..4b427f8 --- /dev/null +++ b/src/ir/lower/stmt.zig @@ -0,0 +1,1276 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +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 type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + +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), + .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| { + // = --- (undef_literal) on tuple types: zero-initialize + if (val.data == .undef_literal and !ty.isBuiltin()) { + const ti = self.module.types.get(ty); + 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; + } + } + // 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 → s64 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; + self.force_block_value = true; + const ref = self.lowerExpr(val); + self.force_block_value = saved_fbv; + 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.s64; + 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:s3) -> s3 { ... } + 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 `-> s64` body lowered into + // a `-> s32` caller would coerce 42 to s32 before storing into the s64 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.s64; + // 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.s64; + 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., ?s32 → s32) + 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(); + } + } +} + +pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { + // 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) { + if (self.program_index.global_names.get(asgn.target.data.identifier.name)) |gi| { + self.target_type = gi.ty; + } + } + } else if (asgn.target.data == .index_expr) { + // For array[i] = val, set target_type to the element type + const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object)); + 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; + if (!obj_ty.isBuiltin()) { + const field_name_id = self.module.types.internString(fa.field); + const struct_fields = self.getStructFields(obj_ty); + for (struct_fields) |f| { + if (f.name == field_name_id) { + self.target_type = f.ty; + break; + } + } + } + } + } + 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 + if (!handled) { + if (self.program_index.global_names.get(id.name)) |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; + } + } + + // 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, .s64, obj_ty); + self.storeOrCompound(gep, val, asgn.op, .s64); + } else if (is_special_container and std.mem.eql(u8, fa.field, "ptr")) { + const gep = self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty); + self.storeOrCompound(gep, val, asgn.op, .s64); + } 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 + // (issue 0094 / issue-0083 two-resolver 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 (issue 0094). + _ = 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.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 }; + +/// 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. +/// 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. 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 resolution shared by all three store/ +/// address-of sites — lowerAssignment (single-target store), lowerExprAsPtr +/// (address-of), and lowerMultiAssign (multi-target store) — so they never +/// resolve a field to a different slot or default field 0 (issue 0094). +pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []const u8) ?FieldLvalue { + 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) { + const ptr = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); + return .{ .ptr = ptr, .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) { + const ug = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); + const ptr = self.builder.structGepTyped(ug, @intCast(si), self.module.types.ptrTo(sf.ty), f.ty); + return .{ .ptr = ptr, .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| { + const elem_ty = tup.fields[idx]; + const ptr = self.builder.structGepTyped(obj_ptr, @intCast(idx), self.module.types.ptrTo(elem_ty), obj_ty); + return .{ .ptr = ptr, .ty = elem_ty }; + } + 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; + const elem_ty = type_info.vector.element; + const ptr = self.builder.structGepTyped(obj_ptr, vidx, self.module.types.ptrTo(elem_ty), obj_ty); + return .{ .ptr = ptr, .ty = elem_ty }; + } + + // Plain struct field. + const struct_fields = self.getStructFields(obj_ty); + for (struct_fields, 0..) |f, i| { + if (f.name == field_name_id) { + const ptr = self.builder.structGepTyped(obj_ptr, @intCast(i), self.module.types.ptrTo(f.ty), obj_ty); + return .{ .ptr = ptr, .ty = f.ty }; + } + } + return null; +} + +/// 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.program_index.global_names.get(id.name)) |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 .s64; + // that bogus pointer reaches LLVM emission as ptrTo(.unresolved) + // and panics (issue 0094). + 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.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), + }; +} + +// ── Expression lowering ───────────────────────────────────────── + +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); +} + +/// 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 saved_target = self.target_type; + self.target_type = ctx_ty; + const ctx_val = self.lowerExpr(ps.context_expr); + self.target_type = saved_target; + + const slot = self.builder.alloca(ctx_ty); + 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.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); + // 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) (issue 0094). + 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; + self.force_block_value = true; + const ref = self.lowerExpr(dd.value); + self.force_block_value = saved_fbv; + 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(); +} + +// ── Comptime lowering ────────────────────────────────────────────