Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
399 lines
18 KiB
Zig
399 lines
18 KiB
Zig
// Tests for resolver.zig — the shared author-collection layer.
|
|
//
|
|
// collectVisibleAuthors is exercised over REAL Phase A facts (parse →
|
|
// resolveImports → buildImportFacts, the exact path core.zig drives) plus one
|
|
// synthetic diamond fixture for pointer-identity dedup. The visibility-adapter
|
|
// tests pin the nameVisibleOverEdges edge-walk that isNameVisible /
|
|
// isCImportVisible run on top of — the flat-edge set vs the full import_graph.
|
|
// resolveBare / resolveQualified tests pin the verdict layer (own_wins / single /
|
|
// ambiguous / not_visible / domain_filtered) over real import facts.
|
|
|
|
const std = @import("std");
|
|
const ast = @import("../ast.zig");
|
|
const parser = @import("../parser.zig");
|
|
const imports = @import("../imports.zig");
|
|
const errors = @import("../errors.zig");
|
|
const resolver = @import("resolver.zig");
|
|
const lower = @import("lower.zig");
|
|
const pi = @import("program_index.zig");
|
|
const ProgramIndex = pi.ProgramIndex;
|
|
|
|
var g_test_threaded: ?std.Io.Threaded = null;
|
|
fn testIo() std.Io {
|
|
if (g_test_threaded == null) {
|
|
g_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{});
|
|
}
|
|
return g_test_threaded.?.io();
|
|
}
|
|
|
|
const Graph = std.StringHashMap(std.StringHashMap(void));
|
|
|
|
/// Parse `main_path`, resolve its imports, build the raw facts, and ALSO keep
|
|
/// the import / flat-import graphs (the collectors need them). `alloc` must be
|
|
/// an arena that outlives the returned views.
|
|
const Facts = struct {
|
|
decls: imports.ModuleDecls,
|
|
ns_edges: imports.NamespaceEdges,
|
|
import_graph: Graph,
|
|
flat_import_graph: Graph,
|
|
};
|
|
|
|
fn buildFacts(alloc: std.mem.Allocator, io: std.Io, absdir: []const u8, main_path: []const u8) !Facts {
|
|
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 diags = errors.DiagnosticList.init(alloc, main_source, main_path);
|
|
var chain = std.StringHashMap(void).init(alloc);
|
|
var cache = imports.ModuleCache.init(alloc);
|
|
var import_graph = Graph.init(alloc);
|
|
var flat_import_graph = Graph.init(alloc);
|
|
const stdlib_paths = [_][]const u8{};
|
|
|
|
const mod = try imports.resolveImports(
|
|
alloc,
|
|
io,
|
|
root,
|
|
absdir,
|
|
main_path,
|
|
&chain,
|
|
&cache,
|
|
null,
|
|
&diags,
|
|
&stdlib_paths,
|
|
&import_graph,
|
|
&flat_import_graph,
|
|
.{},
|
|
);
|
|
|
|
const facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
|
|
return .{
|
|
.decls = facts.decls,
|
|
.ns_edges = facts.ns_edges,
|
|
.import_graph = import_graph,
|
|
.flat_import_graph = flat_import_graph,
|
|
};
|
|
}
|
|
|
|
fn tag(ref: resolver.RawDeclRef) std.meta.Tag(resolver.RawDeclRef) {
|
|
return std.meta.activeTag(ref);
|
|
}
|
|
|
|
// ── collectVisibleAuthors ────────────────────────────────────────────────
|
|
|
|
// own author present; two distinct flat authors both returned RAW; and the
|
|
// user_bare_flat edge set EXCLUDES a namespaced-only import (reachable only over
|
|
// a non-flat edge).
|
|
test "resolver: collectVisibleAuthors — own author, two distinct flat authors, namespaced edge excluded" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
const io = testIo();
|
|
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "dup :: () -> i64 { 1 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "dup :: () -> i64 { 2 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "p.sx", .data = "secret :: () -> i64 { 9 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = "#import \"a.sx\";\n#import \"b.sx\";\ng :: #import \"p.sx\";\nselfauthored :: () -> i64 { 0 }\nmain :: () -> i32 { 0 }\n" });
|
|
|
|
var dirbuf: [4096]u8 = undefined;
|
|
const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)];
|
|
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
|
|
|
var facts = try buildFacts(alloc, io, absdir, main_path);
|
|
|
|
var idx = ProgramIndex.init(alloc);
|
|
defer idx.deinit();
|
|
idx.module_decls = &facts.decls;
|
|
idx.flat_import_graph = &facts.flat_import_graph;
|
|
idx.import_graph = &facts.import_graph;
|
|
|
|
var r = resolver.Resolver.init(&idx, alloc);
|
|
|
|
// Own author (declared in main itself).
|
|
const own_set = r.collectVisibleAuthors("selfauthored", main_path, .user_bare_flat);
|
|
try std.testing.expect(own_set.own != null);
|
|
try std.testing.expectEqualStrings(main_path, own_set.own.?.source);
|
|
try std.testing.expectEqual(@as(usize, 0), own_set.flat.len);
|
|
try std.testing.expectEqual(@as(usize, 1), own_set.distinctCount());
|
|
|
|
// Two distinct flat authors of `dup` (a.sx and b.sx), returned raw.
|
|
const dup_set = r.collectVisibleAuthors("dup", main_path, .user_bare_flat);
|
|
try std.testing.expect(dup_set.own == null);
|
|
try std.testing.expectEqual(@as(usize, 2), dup_set.flat.len);
|
|
try std.testing.expectEqual(@as(usize, 2), dup_set.distinctCount());
|
|
try std.testing.expectEqual(std.meta.Tag(resolver.RawDeclRef).fn_decl, tag(dup_set.flat[0].raw));
|
|
try std.testing.expectEqual(std.meta.Tag(resolver.RawDeclRef).fn_decl, tag(dup_set.flat[1].raw));
|
|
try std.testing.expect(dup_set.flat[0].raw.fn_decl != dup_set.flat[1].raw.fn_decl);
|
|
|
|
// `secret` is authored only in p.sx, imported NAMESPACED (`g :: #import`).
|
|
// user_bare_flat must NOT see it (p.sx is not a flat edge).
|
|
const flat_secret = r.collectVisibleAuthors("secret", main_path, .user_bare_flat);
|
|
try std.testing.expect(flat_secret.own == null);
|
|
try std.testing.expectEqual(@as(usize, 0), flat_secret.flat.len);
|
|
}
|
|
|
|
// Diamond: the SAME author node is reachable over two flat edges. It must
|
|
// collapse to a single entry (dedup by author identity), not appear twice.
|
|
test "resolver: collectVisibleAuthors — diamond imports of one author dedup to one" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
// One real fn_decl node, shared between two module indices.
|
|
var body = ast.Node{ .span = .{ .start = 0, .end = 0 }, .data = .builtin_expr };
|
|
var shared = ast.Node{
|
|
.span = .{ .start = 0, .end = 0 },
|
|
.data = .{ .fn_decl = .{ .name = "shared", .params = &.{}, .return_type = null, .body = &body } },
|
|
};
|
|
const ref = imports.rawDeclRefOf(&shared).?;
|
|
|
|
var decls = imports.ModuleDecls.init(alloc);
|
|
inline for (.{ "p1", "p2" }) |path| {
|
|
var names = std.StringHashMap(resolver.RawDeclRef).init(alloc);
|
|
try names.put("shared", ref);
|
|
try decls.put(path, .{ .source = path, .names = names });
|
|
}
|
|
|
|
var flat = Graph.init(alloc);
|
|
var from_edges = std.StringHashMap(void).init(alloc);
|
|
try from_edges.put("p1", {});
|
|
try from_edges.put("p2", {});
|
|
try flat.put("from", from_edges);
|
|
|
|
var idx = ProgramIndex.init(alloc);
|
|
defer idx.deinit();
|
|
idx.module_decls = &decls;
|
|
idx.flat_import_graph = ♭
|
|
|
|
var r = resolver.Resolver.init(&idx, alloc);
|
|
const set = r.collectVisibleAuthors("shared", "from", .user_bare_flat);
|
|
try std.testing.expect(set.own == null);
|
|
try std.testing.expectEqual(@as(usize, 1), set.flat.len);
|
|
try std.testing.expectEqual(@as(usize, 1), set.distinctCount());
|
|
try std.testing.expectEqual(@intFromPtr(&shared.data.fn_decl), @intFromPtr(set.flat[0].raw.fn_decl));
|
|
}
|
|
|
|
// ── collectNamespaceAuthors ──────────────────────────────────────────────
|
|
|
|
// Returns a namespace target's members and touches NO graph: the Resolver here
|
|
// has no graphs (or module_decls) wired at all, yet the member is found.
|
|
test "resolver: collectNamespaceAuthors — returns target members, walks no graph" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
const io = testIo();
|
|
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "point.sx", .data = "Point :: struct { x: i64 }\nhelper :: () -> i64 { 0 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = "g :: #import \"point.sx\";\nmain :: () -> i32 { 0 }\n" });
|
|
|
|
var dirbuf: [4096]u8 = undefined;
|
|
const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)];
|
|
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
|
const point_path = try std.fmt.allocPrint(alloc, "{s}/point.sx", .{absdir});
|
|
|
|
var facts = try buildFacts(alloc, io, absdir, main_path);
|
|
|
|
const aliases = facts.ns_edges.get(main_path) orelse return error.MissingNsEdges;
|
|
const target = aliases.get("g") orelse return error.MissingAlias;
|
|
try std.testing.expectEqualStrings(point_path, target.target_module_path);
|
|
|
|
// A Resolver over an EMPTY index — no module_decls, no graphs. If
|
|
// collectNamespaceAuthors touched a graph it would crash / miss; it doesn't.
|
|
var idx = ProgramIndex.init(alloc);
|
|
defer idx.deinit();
|
|
try std.testing.expect(idx.flat_import_graph == null);
|
|
try std.testing.expect(idx.import_graph == null);
|
|
var r = resolver.Resolver.init(&idx, alloc);
|
|
|
|
const pt = r.collectNamespaceAuthors(target, "Point");
|
|
try std.testing.expect(pt.own != null);
|
|
try std.testing.expectEqual(std.meta.Tag(resolver.RawDeclRef).struct_decl, tag(pt.own.?.raw));
|
|
try std.testing.expectEqualStrings(point_path, pt.own.?.source);
|
|
try std.testing.expectEqual(@as(usize, 0), pt.flat.len);
|
|
|
|
const hp = r.collectNamespaceAuthors(target, "helper");
|
|
try std.testing.expect(hp.own != null);
|
|
try std.testing.expectEqual(std.meta.Tag(resolver.RawDeclRef).fn_decl, tag(hp.own.?.raw));
|
|
|
|
const miss = r.collectNamespaceAuthors(target, "Missing");
|
|
try std.testing.expect(miss.own == null);
|
|
try std.testing.expectEqual(@as(usize, 0), miss.distinctCount());
|
|
}
|
|
|
|
// ── visibility predicate (the isNameVisible / isCImportVisible core) ──────
|
|
|
|
// nameVisibleOverEdges is the edge-walk isVisible(.user_bare_flat) runs on (the
|
|
// flat graph). Walked over the flat set vs the full import_graph, the two agree
|
|
// on own + flat names and differ ONLY on a namespaced-only name — the flat set
|
|
// the bare-name predicate uses, contrasted with the over-permissive full set.
|
|
test "resolver: visibility edge-walk — own + flat visible; namespaced-only only under import_graph" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
var scopes = Graph.init(alloc);
|
|
inline for (.{
|
|
.{ "main", &[_][]const u8{ "selfauthored", "g" } },
|
|
.{ "a", &[_][]const u8{"dup"} },
|
|
.{ "p", &[_][]const u8{"secret"} },
|
|
}) |entry| {
|
|
var s = std.StringHashMap(void).init(alloc);
|
|
for (entry[1]) |n| try s.put(n, {});
|
|
try scopes.put(entry[0], s);
|
|
}
|
|
|
|
// Flat graph: main flat-imports a only. Import graph: main reaches a + p.
|
|
var flat = Graph.init(alloc);
|
|
var flat_edges = std.StringHashMap(void).init(alloc);
|
|
try flat_edges.put("a", {});
|
|
try flat.put("main", flat_edges);
|
|
|
|
var all = Graph.init(alloc);
|
|
var all_edges = std.StringHashMap(void).init(alloc);
|
|
try all_edges.put("a", {});
|
|
try all_edges.put("p", {});
|
|
try all.put("main", all_edges);
|
|
|
|
// Own-scope name: visible regardless of edge set.
|
|
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, &flat, "main", "selfauthored"));
|
|
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, &all, "main", "selfauthored"));
|
|
|
|
// Flat-imported name: visible under both (the flat edge is in both graphs).
|
|
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, &flat, "main", "dup"));
|
|
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, &all, "main", "dup"));
|
|
|
|
// Namespaced-only name: NOT visible under the flat set (user_bare_flat),
|
|
// but visible under the full import_graph set.
|
|
try std.testing.expect(!lower.nameVisibleOverEdges(&scopes, &flat, "main", "secret"));
|
|
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, &all, "main", "secret"));
|
|
|
|
// Unknown name: not visible.
|
|
try std.testing.expect(!lower.nameVisibleOverEdges(&scopes, &flat, "main", "nope"));
|
|
|
|
// Falls open when scoping infra is unwired (null scopes/graph).
|
|
try std.testing.expect(lower.nameVisibleOverEdges(null, &flat, "main", "secret"));
|
|
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, null, "main", "secret"));
|
|
}
|
|
|
|
// ── resolveBare ──────────────────────────────────────────────────────────────
|
|
|
|
// own_wins when the querying module authors the name; single when one flat
|
|
// author exists; ambiguous for ≥2 flat authors; not_visible when the name is
|
|
// authored only over a namespace edge; domain_filtered for a builtin / local /
|
|
// wrong-domain name.
|
|
test "resolver: resolveBare — own_wins / single / ambiguous / not_visible / domain_filtered" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
const io = testIo();
|
|
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "dup :: () -> i64 { 1 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "dup :: () -> i64 { 2 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "ns.sx", .data = "secret :: () -> i64 { 9 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data =
|
|
\\#import "a.sx";
|
|
\\#import "b.sx";
|
|
\\g :: #import "ns.sx";
|
|
\\selfauthored :: () -> i64 { 0 }
|
|
\\main :: () -> i32 { 0 }
|
|
\\
|
|
});
|
|
|
|
var dirbuf: [4096]u8 = undefined;
|
|
const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)];
|
|
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
|
|
|
var facts = try buildFacts(alloc, io, absdir, main_path);
|
|
|
|
var idx = ProgramIndex.init(alloc);
|
|
defer idx.deinit();
|
|
idx.module_decls = &facts.decls;
|
|
idx.flat_import_graph = &facts.flat_import_graph;
|
|
idx.import_graph = &facts.import_graph;
|
|
|
|
var r = resolver.Resolver.init(&idx, alloc);
|
|
|
|
// own_wins: selfauthored is authored in main itself
|
|
const own = r.resolveBare("selfauthored", main_path, .callable);
|
|
try std.testing.expectEqual(resolver.Verdict.own_wins, own.verdict);
|
|
try std.testing.expect(own.set.own != null);
|
|
|
|
// ambiguous: dup is authored in both a.sx and b.sx
|
|
const amb = r.resolveBare("dup", main_path, .callable);
|
|
try std.testing.expectEqual(resolver.Verdict.ambiguous, amb.verdict);
|
|
|
|
// not_visible: secret is authored in ns.sx (namespaced-only import)
|
|
const nv = r.resolveBare("secret", main_path, .callable);
|
|
try std.testing.expectEqual(resolver.Verdict.not_visible, nv.verdict);
|
|
|
|
// domain_filtered: selfauthored exists but is not a type author
|
|
const df = r.resolveBare("selfauthored", main_path, .bare_type);
|
|
try std.testing.expectEqual(resolver.Verdict.domain_filtered, df.verdict);
|
|
|
|
// domain_filtered with empty set: i64 is a builtin, no user author
|
|
const builtin = r.resolveBare("i64", main_path, .bare_type);
|
|
try std.testing.expectEqual(resolver.Verdict.domain_filtered, builtin.verdict);
|
|
try std.testing.expectEqual(@as(usize, 0), builtin.set.distinctCount());
|
|
}
|
|
|
|
// ── resolveQualified ─────────────────────────────────────────────────────────
|
|
|
|
test "resolver: resolveQualified — single for existing member, domain_filtered for missing" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
const io = testIo();
|
|
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "point.sx", .data = "Point :: struct { x: i64 }\nhelper :: () -> i64 { 0 }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = "g :: #import \"point.sx\";\nmain :: () -> i32 { 0 }\n" });
|
|
|
|
var dirbuf: [4096]u8 = undefined;
|
|
const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)];
|
|
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
|
|
|
var facts = try buildFacts(alloc, io, absdir, main_path);
|
|
|
|
var idx = ProgramIndex.init(alloc);
|
|
defer idx.deinit();
|
|
idx.module_decls = &facts.decls;
|
|
idx.flat_import_graph = &facts.flat_import_graph;
|
|
idx.namespace_edges = &facts.ns_edges;
|
|
|
|
var r = resolver.Resolver.init(&idx, alloc);
|
|
|
|
const aliases = facts.ns_edges.get(main_path) orelse return error.MissingNsEdges;
|
|
const target = aliases.get("g") orelse return error.MissingAlias;
|
|
|
|
// Point is a struct — namespace_member eligible
|
|
const pt = r.resolveQualified(target, "Point");
|
|
try std.testing.expectEqual(resolver.Verdict.single, pt.verdict);
|
|
try std.testing.expect(pt.set.own != null);
|
|
try std.testing.expectEqual(
|
|
std.meta.Tag(resolver.RawDeclRef).struct_decl,
|
|
std.meta.activeTag(pt.set.own.?.raw),
|
|
);
|
|
try std.testing.expectEqual(@as(usize, 0), pt.set.flat.len);
|
|
|
|
// helper is a fn — namespace_member eligible
|
|
const hp = r.resolveQualified(target, "helper");
|
|
try std.testing.expectEqual(resolver.Verdict.single, hp.verdict);
|
|
|
|
// Missing member → domain_filtered with empty set
|
|
const miss = r.resolveQualified(target, "Missing");
|
|
try std.testing.expectEqual(resolver.Verdict.domain_filtered, miss.verdict);
|
|
try std.testing.expectEqual(@as(usize, 0), miss.set.distinctCount());
|
|
}
|