feat(lower): identity-addressable function body lowering [0102b]

Second of four fix-0102 sub-steps. Makes function declaration + body
lowering addressable by decl/FuncId IDENTITY instead of name-first-wins,
so two same-name authors can each carry their OWN body in their OWN
FuncId. Purely additive: the existing name path stays the sole resolver,
so the suite is byte-for-byte unchanged (no call rerouting — that is
0102c).

- declareFunction records `*const FnDecl -> FuncId` in a new identity map
  (`fn_decl_fids`), alongside the existing name-keyed function table.
- Extract the body-lowering tail of lazyLowerFunction into a reusable
  `lowerFunctionBodyInto(fd, fid, name)` that promotes a SPECIFIC extern
  stub into a real body by EXPLICIT FuncId — not by name lookup (which
  returns the first author). The shared save/restore preamble becomes a
  `FnBodyReentry` guard struct, used by both lazyLowerFunction's found
  path and the null-FuncId `ns.fn` alias path; issue-0100 F1/F2 behaviour
  (own-import source context, block_terminated transparency) is preserved.
- Add `lowerRetainedSameNameAuthors`: walks fix-0102a's `module_fns`,
  and for each SHADOWED flat author (a same-name author that is not the
  fn_ast_map winner, in a direct flat import of the main file) declares a
  fresh same-name FuncId + lowers its body in its own module's visibility
  context. FuncId-keyed `lowered_fids` tracks which slots already have a
  body. Not invoked during a default compile (the name path stays the
  default); 0102c wires it into bare-flat-call routing.
- lower.test.zig: regression that compiles two flat-imported modules each
  authoring `greet` and asserts ONE real body before the pass (winner
  only; shadow dropped) and TWO distinct non-extern bodies after — the
  shadowed author is no longer dropped/extern.

Gate (this worktree): zig build, zig build test (400/400),
bash tests/run_examples.sh (457 passed) all green.
This commit is contained in:
agra
2026-06-06 13:02:49 +03:00
parent ccffbbb441
commit 237f794585
2 changed files with 417 additions and 158 deletions

View File

@@ -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.?);
}

View File

@@ -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 `<name>.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 `<pack_name>.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.