test(ffi-linkage): GATE A→B — #foreign ≡ extern IR for fn/global/class (Phase 4)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user