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.?);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user