From 5d4a2c26c1107ab17d78c42046da592ceca288db Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 14 Jun 2026 15:31:09 +0300 Subject: [PATCH] =?UTF-8?q?test(ffi-linkage):=20GATE=20A=E2=86=92B=20?= =?UTF-8?q?=E2=80=94=20#foreign=20=E2=89=A1=20extern=20IR=20for=20fn/globa?= =?UTF-8?q?l/class=20(Phase=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ir/lower.test.zig | 141 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 3da44bc..9a364a5 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -1574,3 +1574,144 @@ test "lower: scan populates source-keyed caches per declaring source (E0)" { const global_k = idx.module_const_map.get("K").?; try std.testing.expect(global_k.value == k_a.value or global_k.value == k_b.value); } + +// ── GATE A→B: `extern`/`export` are a behavior-equivalent superset of `#foreign` ── +// +// The FFI-linkage stream replaces the prefix `#foreign` linkage modifier with the +// postfix `extern` (import) / `export` (define) keywords. Part B may not begin +// migrating `#foreign` call sites until this is locked: a sample fn / global / +// runtime-class written with `#foreign` must lower to byte-identical IR as the +// same decl written with `extern`. This test is the gate — keep it green. + +/// Lower a single self-contained `.sx` source (no stdlib imports) all the way to +/// the printed module IR text. Mirrors the full pipeline in the fix-0102b tests +/// above (parse → resolveImports → lowerRoot → printModule). Arena-allocated; +/// the caller's arena owns everything. +fn lowerSrcToIr(alloc: std.mem.Allocator, io: std.Io, src: []const u8) ![]const u8 { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = 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_source = try alloc.dupeZ(u8, src); + 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, + .{}, + ); + + 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 facts = try imports.buildImportFacts(alloc, main_path, mod, &cache); + + const resolved_root = try alloc.create(Node); + resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } }; + + var module = ir_mod.Module.init(alloc); + // NOTE: no `defer module.deinit()` — the printed IR is duped into the arena + // before return, and the arena owns the rest; deiniting here is unnecessary. + 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_decls = &facts.decls; + + lowering.lowerRoot(resolved_root); + if (diagnostics.hasErrors()) return error.LoweringFailed; + + const print_mod = @import("print.zig"); + var aw = std.Io.Writer.Allocating.init(alloc); + try print_mod.printModule(&module, &aw.writer); + const result = aw.writer.toArrayList(); + return try alloc.dupe(u8, result.items); +} + +test "lower: GATE A→B — #foreign and extern/export lower to identical IR" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + const io = lowerTestIo(); + + // 1. FUNCTION — bare libc-style import. + { + const foreign_ir = try lowerSrcToIr(alloc, io, + \\f :: (a: i32) -> i32 #foreign; + \\main :: () -> i32 { f(7) } + \\ + ); + const extern_ir = try lowerSrcToIr(alloc, io, + \\f :: (a: i32) -> i32 extern; + \\main :: () -> i32 { f(7) } + \\ + ); + try std.testing.expectEqualStrings(foreign_ir, extern_ir); + } + + // 2. DATA GLOBAL — extern global. + { + const foreign_ir = try lowerSrcToIr(alloc, io, + \\g : *void #foreign; + \\main :: () -> i64 { xx g } + \\ + ); + const extern_ir = try lowerSrcToIr(alloc, io, + \\g : *void extern; + \\main :: () -> i64 { xx g } + \\ + ); + try std.testing.expectEqualStrings(foreign_ir, extern_ir); + } + + // 3. RUNTIME CLASS — Obj-C reference (import) class with dispatch. + { + const foreign_ir = try lowerSrcToIr(alloc, io, + \\C :: #foreign #objc_class("NSObject") { + \\ alloc :: () -> *C; + \\ init :: (self: *Self) -> *Self; + \\} + \\main :: () -> i32 { a := C.alloc().init(); if a != null { return 1; } 0 } + \\ + ); + const extern_ir = try lowerSrcToIr(alloc, io, + \\C :: #objc_class("NSObject") extern { + \\ alloc :: () -> *C; + \\ init :: (self: *Self) -> *Self; + \\} + \\main :: () -> i32 { a := C.alloc().init(); if a != null { return 1; } 0 } + \\ + ); + try std.testing.expectEqualStrings(foreign_ir, extern_ir); + } +}