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:
@@ -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.?);
|
||||
}
|
||||
|
||||
438
src/ir/lower.zig
438
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 `<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.
|
||||
|
||||
Reference in New Issue
Block a user