diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 765e36d..7a97a16 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -10,6 +10,9 @@ const Ref = ir_mod.Ref; const FuncId = ir_mod.FuncId; const Lowering = ir_mod.Lowering; +const parser = @import("../parser.zig"); +const imports = @import("../imports.zig"); + test "lower: simple function with arithmetic" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); @@ -1264,3 +1267,137 @@ test "lower: reflectionArgIsType accepts spelled types, rejects plain values (is try std.testing.expect(!l.reflectionArgIsType(&float_node)); try std.testing.expect(!l.reflectionArgIsType(&bool_node)); } + +var g_lower_test_threaded: ?std.Io.Threaded = null; +fn lowerTestIo() std.Io { + if (g_lower_test_threaded == null) { + g_lower_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{}); + } + return g_lower_test_threaded.?.io(); +} + +/// Count functions named `name` that carry a REAL body (promoted from the extern +/// stub: not `is_extern`, at least one basic block). +fn countRealBodies(module: *ir_mod.Module, name: []const u8) usize { + var n: usize = 0; + for (module.functions.items) |func| { + if (!std.mem.eql(u8, module.types.getString(func.name), name)) continue; + if (func.is_extern) continue; + if (func.blocks.items.len == 0) continue; + n += 1; + } + return n; +} + +// fix-0102b: two flat-imported modules each author `greet`. The first-wins merge +// keeps a.sx's author in the merged decl list (the WINNER — lowered when `main` +// calls `greet()`) and drops b.sx's, which `module_fns` still retains (0102a). +// BEFORE the identity-addressable pass, only the winner has a real body — the +// shadowed author has no slot at all (the pre-fix symptom: one `greet`). +// `lowerRetainedSameNameAuthors` declares the shadowed author its OWN same-name +// FuncId and lowers its body there, so BOTH authors carry distinct, non-extern +// bodies. Call resolution is untouched: `resolveFuncByName` still returns the +// winner, so `main`'s `greet()` binds first-wins (rerouting is fix-0102c). +test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102b)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + const io = lowerTestIo(); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "greet :: () -> s64 { 1 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "greet :: () -> s64 { 2 }\n" }); + const main_src = + \\#import "a.sx"; + \\#import "b.sx"; + \\main :: () -> s64 { greet() } + \\ + ; + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = main_src }); + + var dirbuf: [4096]u8 = undefined; + const dirlen = try tmp.dir.realPath(io, &dirbuf); + const absdir = dirbuf[0..dirlen]; + + const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir}); + const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20)); + const main_source = try alloc.dupeZ(u8, main_bytes); + var p = parser.Parser.init(alloc, main_source); + const root = p.parse() catch return error.ParseFailed; + + var chain = std.StringHashMap(void).init(alloc); + var cache = imports.ModuleCache.init(alloc); + var import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); + var flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); + const stdlib_paths = [_][]const u8{}; + + const mod = try imports.resolveImports( + alloc, + io, + root, + absdir, + main_path, + &chain, + &cache, + null, + null, + &stdlib_paths, + &import_graph, + &flat_import_graph, + .{}, + ); + + // Per-module visibility scopes + authored-function index, wired exactly as + // `core.zig` does before `lowerRoot`. + var module_scopes = std.StringHashMap(std.StringHashMap(void)).init(alloc); + try module_scopes.put(main_path, mod.scope); + var cache_it = cache.iterator(); + while (cache_it.next()) |entry| { + try module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope); + } + var module_fns = imports.ModuleFns.init(alloc); + try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns); + + const resolved_root = try alloc.create(Node); + resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } }; + + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var diagnostics = errors.DiagnosticList.init(alloc, main_source, main_path); + var lowering = Lowering.init(&module); + lowering.main_file = main_path; + lowering.resolved_root = resolved_root; + lowering.diagnostics = &diagnostics; + lowering.program_index.module_scopes = &module_scopes; + lowering.program_index.import_graph = &import_graph; + lowering.program_index.flat_import_graph = &flat_import_graph; + lowering.program_index.module_fns = &module_fns; + + lowering.lowerRoot(resolved_root); + try std.testing.expect(!diagnostics.hasErrors()); + + // Pre-fix symptom: only the winner `greet` (a.sx) has a real body — lowered + // because `main` calls it; the shadowed author (b.sx) was dropped entirely. + try std.testing.expectEqual(@as(usize, 1), countRealBodies(&module, "greet")); + + // Identity-addressable pass: the shadowed author gets its OWN FuncId + body. + lowering.lowerRetainedSameNameAuthors(); + try std.testing.expect(!diagnostics.hasErrors()); + + // Both `greet` authors now carry distinct, real (non-extern) bodies, and the + // two FuncIds are distinct. + try std.testing.expectEqual(@as(usize, 2), countRealBodies(&module, "greet")); + + const name_id = module.types.internString("greet"); + var first: ?FuncId = null; + var second: ?FuncId = null; + for (module.functions.items, 0..) |func, i| { + if (func.name != name_id) continue; + if (func.is_extern or func.blocks.items.len == 0) continue; + if (first == null) first = FuncId.fromIndex(@intCast(i)) else second = FuncId.fromIndex(@intCast(i)); + } + try std.testing.expect(first != null and second != null); + try std.testing.expect(first.? != second.?); +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 12231f5..f65ec13 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -126,6 +126,17 @@ pub const Lowering = struct { comptime_param_nodes: ?std.StringHashMap(*const Node) = null, // active comptime substitutions target_type: ?TypeId = null, // target type for struct/enum literals without explicit names lowered_functions: std.StringHashMap(void), // tracks which functions have been fully lowered + /// Identity map: authoring `*const ast.FnDecl` → the FuncId `declareFunction` + /// created for it. The name-keyed function table (`resolveFuncByName`) returns + /// the FIRST author of a name, so two same-name authors collide there; this + /// map addresses each author's OWN slot by decl identity (fix-0102b), letting + /// a SHADOWED author lower its body into a distinct FuncId. + fn_decl_fids: std.AutoHashMap(*const ast.FnDecl, FuncId), + /// FuncId-keyed lowered tracking — the identity twin of `lowered_functions` + /// (which keys by name). A shadowed same-name author shares the winner's name + /// but not its FuncId, so name-keyed tracking can't tell them apart; this + /// records which specific FuncIds have had a real body lowered (fix-0102b). + lowered_fids: std.AutoHashMap(FuncId, void), local_fn_counter: u32 = 0, // unique counter for mangling local function names /// Declaration-name / import / visibility facts (architecture phase A1, /// `ProgramIndex`). Owns `import_flags`; borrows `module_scopes` / @@ -286,12 +297,89 @@ pub const Lowering = struct { ret_var_name: ?[]const u8, }; + /// Caller-state protection for lowering a function body re-entrantly — a + /// lazily lowered callee, a qualified `ns.fn` alias, or an out-of-line + /// same-name author. `enter` snapshots the in-progress builder / scope / + /// flag / pack / jni state and installs a fresh set for the nested body; + /// `restore` puts the caller's state back. Lowering a callee must be + /// transparent to the caller's own lowering — notably `block_terminated`, + /// which leaking back would mark the caller's trailing statements + /// dead-after-terminator (issue 0100 F2). + const FnBodyReentry = struct { + l: *Lowering, + func: ?FuncId, + block: ?BlockId, + counter: u32, + scope: ?*Scope, + defer_base: usize, + block_terminated: bool, + force_block_value: bool, + source_file: ?[]const u8, + jni_env_base: usize, + pack_arg_nodes: ?std.StringHashMap([]const *const Node), + pack_param_count: ?std.StringHashMap(u32), + pack_arg_types: ?std.StringHashMap([]const TypeId), + inline_return_target: ?InlineReturnInfo, + + fn enter(l: *Lowering) FnBodyReentry { + const g = FnBodyReentry{ + .l = l, + .func = l.builder.func, + .block = l.builder.current_block, + .counter = l.builder.inst_counter, + .scope = l.scope, + .defer_base = l.func_defer_base, + .block_terminated = l.block_terminated, + .force_block_value = l.force_block_value, + .source_file = l.current_source_file, + .jni_env_base = l.jni_env_stack_base, + .pack_arg_nodes = l.pack_arg_nodes, + .pack_param_count = l.pack_param_count, + .pack_arg_types = l.pack_arg_types, + .inline_return_target = l.inline_return_target, + }; + // The `#jni_env` Ref stack is lexical to ONE function's instruction + // stream; move the visible base to the current top. Pack-fn mono + // state is likewise lexical to the pack-fn body — null it so a + // callee sharing a param NAME with the active pack doesn't fold the + // outer mono's arity into its own `.len`. + l.jni_env_stack_base = l.jni_env_stack.items.len; + l.pack_arg_nodes = null; + l.pack_param_count = null; + l.pack_arg_types = null; + l.inline_return_target = null; + l.func_defer_base = l.defer_stack.items.len; + l.block_terminated = false; + l.force_block_value = false; + return g; + } + + fn restore(g: FnBodyReentry) void { + const l = g.l; + l.setCurrentSourceFile(g.source_file); + l.scope = g.scope; + l.func_defer_base = g.defer_base; + l.block_terminated = g.block_terminated; + l.force_block_value = g.force_block_value; + l.builder.func = g.func; + l.builder.current_block = g.block; + l.builder.inst_counter = g.counter; + l.jni_env_stack_base = g.jni_env_base; + l.pack_arg_nodes = g.pack_arg_nodes; + l.pack_param_count = g.pack_param_count; + l.pack_arg_types = g.pack_arg_types; + l.inline_return_target = g.inline_return_target; + } + }; + pub fn init(module: *Module) Lowering { return .{ .module = module, .builder = Builder.init(module), .alloc = module.alloc, .lowered_functions = std.StringHashMap(void).init(module.alloc), + .fn_decl_fids = std.AutoHashMap(*const ast.FnDecl, FuncId).init(module.alloc), + .lowered_fids = std.AutoHashMap(FuncId, void).init(module.alloc), .program_index = ProgramIndex.init(module.alloc), }; } @@ -1367,6 +1455,82 @@ pub const Lowering = struct { } } + /// Lower every SHADOWED same-name function author into its OWN FuncId with a + /// real (non-extern) body — the identity-addressable lowering PATH this step + /// adds (fix-0102b). It does NOT run during a default compile: the name path + /// stays the sole resolver, so the suite is byte-for-byte unchanged. fix-0102c + /// invokes it as part of routing bare flat calls to the right author; until + /// then it is exercised by the lower-test regression that asserts two distinct + /// non-extern bodies for a same-name collision. + /// + /// The first-wins flat/directory merge keeps exactly one author per name in + /// the merged decl list; `scanDecls` declares that WINNER (lowered on demand + /// through the name-keyed `lazyLowerFunction`). fix-0102a retained every + /// dropped same-name author in `module_fns` (path → name → `*FnDecl`) without + /// touching resolution; this walks that index and gives each shadowed author + /// its own slot: `declareFunction` (identity-mapped to a fresh same-name + /// FuncId) + `lowerFunctionBodyInto` (its body, in its own module's + /// visibility context). Two same-name authors then carry distinct FuncIds and + /// distinct bodies, while `resolveFuncByName` still returns the first (winner) + /// author so existing calls bind first-wins. + /// + /// Scoped to DIRECT flat imports of the main file: a `module_fns` entry whose + /// path is the main file or one of its bare `#import` edges. A namespaced + /// (`ns :: #import`) author has no bare-name winner and is excluded both by + /// that flat-edge gate and by the `fn_ast_map` winner lookup below. + pub fn lowerRetainedSameNameAuthors(self: *Lowering) void { + const module_fns = self.program_index.module_fns orelse return; + const main_file = self.main_file orelse return; + const flat_graph = self.program_index.flat_import_graph orelse return; + const main_flat_edges = flat_graph.get(main_file); + + var path_it = module_fns.iterator(); + while (path_it.next()) |path_entry| { + const path = path_entry.key_ptr.*; + const is_eligible = std.mem.eql(u8, path, main_file) or + (main_flat_edges != null and main_flat_edges.?.contains(path)); + if (!is_eligible) continue; + + var fn_it = path_entry.value_ptr.iterator(); + while (fn_it.next()) |fn_entry| { + const name = fn_entry.key_ptr.*; + const fd = fn_entry.value_ptr.*; + + // A name with no bare winner is namespaced-only (`ns.fn`) — it + // never participated in the flat merge, so it has no shadow to + // lower. The author already owning the name-keyed slot (the + // first-wins winner) lowers through the normal lazy path. + const winner = self.program_index.fn_ast_map.get(name) orelse continue; + if (winner == fd) continue; + + // Only plain free functions get an out-of-line slot; generic / + // foreign / builtin / #compiler authors keep their existing + // dispatch (mirrors lazyLowerFunction / declareFunction guards). + if (fd.type_params.len > 0) continue; + switch (fd.body.data) { + .foreign_expr, .builtin_expr, .compiler_expr => continue, + else => {}, + } + + // Already given its own slot + body? (idempotent across reruns.) + if (self.fn_decl_fids.get(fd)) |existing| { + if (self.lowered_fids.contains(existing)) continue; + } + + // Declare a fresh same-name FuncId for this author and lower its + // body in its OWN module's visibility context (the path key IS + // the author's source file, matching `module_scopes`). + const saved_src = self.current_source_file; + self.setCurrentSourceFile(path); + if (!self.fn_decl_fids.contains(fd)) self.declareFunction(fd, name); + self.setCurrentSourceFile(saved_src); + const fid = self.fn_decl_fids.get(fd) orelse continue; + self.lowerFunctionBodyInto(fd, fid, name); + self.lowered_fids.put(fid, {}) catch {}; + } + } + } + /// Declare a function as an extern stub (signature only, no body). pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void { // Skip generic templates — they're monomorphized on demand, not declared as extern @@ -1421,6 +1585,7 @@ pub const Lowering = struct { func.is_variadic = is_variadic; func.has_implicit_ctx = wants_ctx; self.foreign_name_map.put(name, c_name) catch {}; + self.fn_decl_fids.put(fd, fid) catch {}; return; } } @@ -1432,6 +1597,7 @@ pub const Lowering = struct { func.source_file = self.current_source_file; func.is_variadic = is_variadic; func.has_implicit_ctx = wants_ctx; + self.fn_decl_fids.put(fd, fid) catch {}; } /// Register a namespaced import's OWN functions under their module-qualified @@ -1581,73 +1747,11 @@ pub const Lowering = struct { // Mark as lowered before lowering (prevents infinite recursion) self.lowered_functions.put(name, {}) catch {}; - // Save builder state (same pattern as lambda lowering) - const saved_func = self.builder.func; - const saved_block = self.builder.current_block; - const saved_counter = self.builder.inst_counter; - const saved_scope = self.scope; - const saved_defer_base = self.func_defer_base; - const saved_block_terminated = self.block_terminated; - const saved_force_block_value = self.force_block_value; - const saved_source_file = self.current_source_file; - // Lowering a callee must be transparent to the caller's lowering - // state: restore the FULL saved context on EVERY exit path through one - // defer so the three exits (non-null branch, already-promoted early - // return, null-FuncId `ns.fn` alias branch) cannot drift. Notably - // `block_terminated` — a qualified alias whose body terminates (e.g. a - // constant-folded `if true { return … }`) leaves it true, and leaking - // that into the caller marks the caller's own trailing statements - // dead-after-terminator (issue 0100 F2). The jni/pack/foreign-class - // fields keep their own defers above. - defer { - self.setCurrentSourceFile(saved_source_file); - self.scope = saved_scope; - self.func_defer_base = saved_defer_base; - self.block_terminated = saved_block_terminated; - self.force_block_value = saved_force_block_value; - self.builder.func = saved_func; - self.builder.current_block = saved_block; - self.builder.inst_counter = saved_counter; - } - // The `#jni_env` Ref stack is lexical within ONE function's instruction - // stream — Refs from the caller don't dereference correctly in this - // callee's body. Move the visible base to the current top so - // omitted-env `#jni_call` in this fn doesn't accidentally pick up the - // caller's Refs. Defer covers all the early-return paths below. - const saved_jni_env_base = self.jni_env_stack_base; - self.jni_env_stack_base = self.jni_env_stack.items.len; - defer self.jni_env_stack_base = saved_jni_env_base; - // Pack-fn mono state is lexical to the pack-fn body. A lazily - // lowered callee may share a param NAME with the active pack - // (e.g. `walk(args: []Any)` called from `probe(..$args)`); without - // isolation, `lowerFieldAccess`'s `.len` intercept - // folds the callee's `args.len` to the outer mono's arity and - // bakes the constant into the IR. Same shape for the AST-node - // and per-element-type maps. Null out for the duration of the - // body lowering and restore on exit. - const saved_pan = self.pack_arg_nodes; - const saved_ppc = self.pack_param_count; - const saved_pat = self.pack_arg_types; - const saved_iri = self.inline_return_target; - self.pack_arg_nodes = null; - self.pack_param_count = null; - self.pack_arg_types = null; - self.inline_return_target = null; - defer { - self.pack_arg_nodes = saved_pan; - self.pack_param_count = saved_ppc; - self.pack_arg_types = saved_pat; - self.inline_return_target = saved_iri; - } - self.func_defer_base = self.defer_stack.items.len; - self.block_terminated = false; - self.force_block_value = false; - - // Find the existing extern stub and replace it with a full body + // Find the existing extern stub (from scanDecls), keyed by NAME — the + // FIRST author of a name owns this slot. A shadowed same-name author is + // not here (it has no name-keyed slot); it is lowered out-of-line into + // its OWN FuncId by `lowerRetainedSameNameAuthors` (fix-0102b). const name_id = self.module.types.internString(name); - const ret_ty = self.resolveReturnType(fd); - - // Look up the existing function declaration (from scanDecls) var func_id: ?FuncId = null; for (self.module.functions.items, 0..) |func, i| { if (func.name == name_id) { @@ -1656,100 +1760,118 @@ pub const Lowering = struct { } } - if (func_id == null) { - // Function not yet declared — create it fresh via lowerFunction. - // A module-qualified alias (`ns.fn`, issue 0100) is registered in - // `fn_ast_map` without an eager `declareFunction`, so there's no - // `Function.source_file` to switch to (the path above). Restore the - // alias's OWN declaring source before lowering its body, otherwise - // it lowers in the caller's visibility context and an own-import - // callee (`foo` calling `helper` from `foo`'s module's flat import) - // is reported "not visible" (issue 0100 F1). - if (self.program_index.qualified_fn_source.get(name)) |src| { - self.setCurrentSourceFile(src); - } - self.lowerFunction(fd, name, false); - return; // caller state restored by the top-level defer - } - if (func_id) |fid| { - // Re-use the existing function slot — switch builder to it - self.builder.func = fid; - const func = &self.module.functions.items[@intFromEnum(fid)]; - self.setCurrentSourceFile(func.source_file); - if (!func.is_extern) { - // Already promoted (e.g., via lowerComptimeDeps) — skip. - // Caller state restored by the top-level defer. - return; - } - func.is_extern = false; // promote from extern stub to real function - func.linkage = if (isExportedEntryName(name)) .external else .internal; - if (fd.call_conv == .c) func.call_conv = .c; - // Set inst_counter to param count (params occupy refs 0..N-1). - // IR params = AST params + 1 if the function carries `__sx_ctx` - // at slot 0. - const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0; - std.debug.assert(func.params.len == fd.params.len + ctx_slots); - self.builder.inst_counter = @intCast(func.params.len); - - // Create entry block - const entry_name = self.module.types.internString("entry"); - const entry = self.builder.appendBlock(entry_name, &.{}); - self.builder.switchToBlock(entry); - - // Create scope and bind params - var scope = Scope.init(self.alloc, null); - defer scope.deinit(); - self.scope = &scope; - - // The implicit `__sx_ctx` param (when present) lives at slot 0; - // user params shift by one. `current_ctx_ref` is bound to slot 0 - // so call-site lowering can prepend it to every sx-to-sx call. - // For OS-called entry points (main / JNI hooks), there's no - // ctx param at all — we synthesise `&__sx_default_context` and - // bind `current_ctx_ref` to its address so the body's sx-to-sx - // calls have a sensible Context to forward. - const wants_ctx = self.funcWantsImplicitCtx(fd); - const saved_ctx_ref = self.current_ctx_ref; - defer self.current_ctx_ref = saved_ctx_ref; - const user_param_base: u32 = if (wants_ctx) 1 else 0; - if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); - - for (fd.params, 0..) |p, i| { - const pty = self.resolveParamType(&p); - const slot = self.builder.alloca(pty); - const param_ref = Ref.fromIndex(@intCast(i + user_param_base)); - self.builder.store(slot, param_ref); - scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); - } - - // Inbound entry points and callconv(.c) sx functions: bind - // current_ctx_ref to the static default before any user code - // runs. C-callable sx functions don't get a __sx_ctx param, - // but their bodies may call ctx-aware sx functions / fn-ptrs - // and need a real Context to forward. - if (!wants_ctx and self.implicit_ctx_enabled) { - if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| { - self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void)); - } - } - - // Lower the function body (set target_type to return type for implicit returns) - const saved_target = self.target_type; - self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null; - if (ret_ty != .void and ret_ty != .noreturn) { - self.lowerValueBody(fd.body, ret_ty); - } else { - // void / noreturn: no value to return — lower as statements and - // let `ensureTerminator` close the block (ret void / unreachable). - self.lowerBlock(fd.body); - self.ensureTerminator(ret_ty); - } - self.target_type = saved_target; - - self.builder.finalize(); + self.lowerFunctionBodyInto(fd, fid, name); + return; } - // Caller state restored by the top-level defer. + + // Function not yet declared — create it fresh via lowerFunction. A + // module-qualified alias (`ns.fn`, issue 0100) is registered in + // `fn_ast_map` without an eager `declareFunction`, so there's no + // `Function.source_file` to switch to. Restore the alias's OWN declaring + // source before lowering its body, otherwise it lowers in the caller's + // visibility context and an own-import callee (`foo` calling `helper` + // from `foo`'s module's flat import) is reported "not visible" (0100 F1). + // The reentry guard keeps the nested lowering transparent to the caller. + var reentry = FnBodyReentry.enter(self); + defer reentry.restore(); + if (self.program_index.qualified_fn_source.get(name)) |src| { + self.setCurrentSourceFile(src); + } + self.lowerFunction(fd, name, false); + } + + /// Lower `fd`'s body into the SPECIFIC `fid`, promoting its extern stub to a + /// real function. Identity-addressable: the caller passes the exact FuncId, + /// so a SHADOWED same-name author lowers into its OWN slot instead of + /// colliding on the name-keyed `resolveFuncByName` (which returns the first + /// author, the very split that trips issue 0100's param-count assert). Self- + /// contained — the `FnBodyReentry` guard makes the nested lowering + /// transparent to any in-progress caller body (issue 0100 F2) — so it serves + /// both `lazyLowerFunction`'s name-keyed found path and the out-of-line + /// `lowerRetainedSameNameAuthors` pass. + fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId, name: []const u8) void { + // objc-defined-class method context for `*Self` substitution (M1.2 A.2b); + // the resolveReturnType / resolveParamType calls below consult it. + const saved_fc = self.current_foreign_class; + defer self.current_foreign_class = saved_fc; + if (self.lookupObjcDefinedClassForMethod(name)) |fcd| { + self.current_foreign_class = fcd; + } + + var reentry = FnBodyReentry.enter(self); + defer reentry.restore(); + + const ret_ty = self.resolveReturnType(fd); + + // Re-use the existing function slot — switch builder to it. + self.builder.func = fid; + const func = &self.module.functions.items[@intFromEnum(fid)]; + self.setCurrentSourceFile(func.source_file); + if (!func.is_extern) { + // Already promoted (e.g., via lowerComptimeDeps) — skip. + return; + } + func.is_extern = false; // promote from extern stub to real function + func.linkage = if (isExportedEntryName(name)) .external else .internal; + if (fd.call_conv == .c) func.call_conv = .c; + // Set inst_counter to param count (params occupy refs 0..N-1). IR params + // = AST params + 1 if the function carries `__sx_ctx` at slot 0. + const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0; + std.debug.assert(func.params.len == fd.params.len + ctx_slots); + self.builder.inst_counter = @intCast(func.params.len); + + // Create entry block + const entry_name = self.module.types.internString("entry"); + const entry = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry); + + // Create scope and bind params + var scope = Scope.init(self.alloc, null); + defer scope.deinit(); + self.scope = &scope; + + // The implicit `__sx_ctx` param (when present) lives at slot 0; user + // params shift by one. `current_ctx_ref` is bound to slot 0 so call-site + // lowering can prepend it to every sx-to-sx call. For OS-called entry + // points (main / JNI hooks) there's no ctx param — synthesise + // `&__sx_default_context` and bind `current_ctx_ref` to its address. + const wants_ctx = self.funcWantsImplicitCtx(fd); + const saved_ctx_ref = self.current_ctx_ref; + defer self.current_ctx_ref = saved_ctx_ref; + const user_param_base: u32 = if (wants_ctx) 1 else 0; + if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); + + for (fd.params, 0..) |p, i| { + const pty = self.resolveParamType(&p); + const slot = self.builder.alloca(pty); + const param_ref = Ref.fromIndex(@intCast(i + user_param_base)); + self.builder.store(slot, param_ref); + scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); + } + + // Inbound entry points + callconv(.c) sx functions: bind current_ctx_ref + // to the static default before any user code runs. + if (!wants_ctx and self.implicit_ctx_enabled) { + if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| { + self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void)); + } + } + + // Lower the function body (set target_type to return type for implicit returns) + const saved_target = self.target_type; + self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null; + if (ret_ty != .void and ret_ty != .noreturn) { + self.lowerValueBody(fd.body, ret_ty); + } else { + // void / noreturn: no value to return — lower as statements and let + // `ensureTerminator` close the block (ret void / unreachable). + self.lowerBlock(fd.body); + self.ensureTerminator(ret_ty); + } + self.target_type = saved_target; + + self.builder.finalize(); } /// Lower a single function declaration.