Files
sx/src/ir/protocols.test.zig
agra d8076b9333 lang: rename signed integer types sN -> iN
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.
2026-06-12 09:31:53 +03:00

285 lines
13 KiB
Zig

// Tests for protocols.zig — the protocol/impl LOOKUP owner (`ProtocolResolver`).
// Reached via `ir.ProtocolResolver{ .l = &lowering }`, mirroring calls.test.zig /
// generics.test.zig. Covers the pure conformance queries moved out of `Lowering`
// in A4.2 sub-step 2 (lookup increment); registration + emission stay in
// `Lowering`, so their plan tests land with later increments.
const std = @import("std");
const ast = @import("../ast.zig");
const Node = ast.Node;
const errors = @import("../errors.zig");
const ir_mod = @import("ir.zig");
const TypeId = ir_mod.TypeId;
const FuncId = ir_mod.FuncId;
const Lowering = ir_mod.Lowering;
const ProtocolResolver = ir_mod.ProtocolResolver;
fn protoMethodReq(name: []const u8) ast.ProtocolMethodDecl {
// A required (no default body) method, no params, void return.
return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null };
}
fn mk(alloc: std.mem.Allocator, data: ast.Node.Data) *Node {
const n = alloc.create(Node) catch unreachable;
n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data };
return n;
}
fn typeExpr(alloc: std.mem.Allocator, name: []const u8) *Node {
return mk(alloc, .{ .type_expr = .{ .name = name, .is_generic = false } });
}
fn emptyBody(alloc: std.mem.Allocator) *Node {
return mk(alloc, .{ .block = .{ .stmts = &.{} } });
}
test "protocols: getProtocolInfo resolves registered protocol structs only" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var l = Lowering.init(&module);
const pr = ProtocolResolver{ .l = &l };
const methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods };
l.registerProtocolDecl(&pd);
// The registered protocol struct resolves to its decl info.
const drawable_ty = module.types.findByName(module.types.internString("Drawable")).?;
const info = pr.getProtocolInfo(drawable_ty).?;
try std.testing.expectEqualStrings("Drawable", info.name);
try std.testing.expectEqual(@as(usize, 1), info.methods.len);
// A builtin and an unrelated plain struct are not protocols.
try std.testing.expect(pr.getProtocolInfo(.i32) == null);
const plain = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Point"), .fields = &.{} } });
try std.testing.expect(pr.getProtocolInfo(plain) == null);
// The Lowering wrapper delegates to the same result.
try std.testing.expect(l.getProtocolInfo(drawable_ty) != null);
}
test "protocols: hasImplPlain reflects materialized thunks for a (protocol, type) pair" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var l = Lowering.init(&module);
const pr = ProtocolResolver{ .l = &l };
const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });
// No thunks yet → not materialized.
try std.testing.expect(!pr.hasImplPlain("Drawable", circle));
// Materialize the (Drawable, Circle) thunk slot the way `getOrCreateThunks`
// does — key "Proto\x00<formatTypeName>". hasImplPlain must then see it.
const key = std.fmt.allocPrint(alloc, "Drawable\x00{s}", .{l.formatTypeName(circle)}) catch unreachable;
l.protocol_thunk_map.put(key, &[_]FuncId{}) catch unreachable;
try std.testing.expect(pr.hasImplPlain("Drawable", circle));
// A different protocol over the same type is still unmaterialized.
try std.testing.expect(!pr.hasImplPlain("Hash", circle));
}
test "protocols: packArgConformsTo at the impl-declaration level (non-parameterised)" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var l = Lowering.init(&module);
const pr = ProtocolResolver{ .l = &l };
// Shape :: protocol { draw :: (); } (non-parameterised, one required method)
const methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
const pd = ast.ProtocolDecl{ .name = "Shape", .methods = &methods };
l.registerProtocolDecl(&pd);
const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });
// No `Circle.draw` registered → does NOT conform.
try std.testing.expect(!pr.packArgConformsTo("Shape", circle));
// Register the impl method `Circle.draw` (how registerImplBlock records a
// non-parameterised impl) → now conforms.
const body = alloc.create(Node) catch unreachable;
body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{} } } };
const draw_fd = ast.FnDecl{ .name = "Circle.draw", .params = &.{}, .return_type = null, .body = body };
l.program_index.fn_ast_map.put("Circle.draw", &draw_fd) catch unreachable;
try std.testing.expect(pr.packArgConformsTo("Shape", circle));
// An arg already erased to the protocol struct itself trivially conforms.
const shape_ty = module.types.findByName(module.types.internString("Shape")).?;
try std.testing.expect(pr.packArgConformsTo("Shape", shape_ty));
// An unregistered protocol name conforms to nothing.
try std.testing.expect(!pr.packArgConformsTo("Nope", circle));
}
test "protocols: registerImplBlock records <Target>.<method> in fn_ast_map" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var l = Lowering.init(&module);
const pr = ProtocolResolver{ .l = &l };
// Drawable :: protocol { draw :: (); } + impl Drawable for Circle { draw :: (){} }
const proto_methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &proto_methods };
l.registerProtocolDecl(&pd);
const draw_node = mk(alloc, .{ .fn_decl = .{ .name = "draw", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
const methods = [_]*Node{draw_node};
const ib = ast.ImplBlock{ .protocol_name = "Drawable", .target_type = "Circle", .methods = &methods };
const decl = mk(alloc, .{ .impl_block = ib });
// Not registered before; the non-parameterised impl registers `Circle.draw`.
try std.testing.expect(!l.program_index.fn_ast_map.contains("Circle.draw"));
pr.registerImplBlock(&ib, false, decl);
try std.testing.expect(l.program_index.fn_ast_map.contains("Circle.draw"));
// And it now conforms (same fn_ast_map entry packArgConformsTo checks).
const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });
try std.testing.expect(pr.packArgConformsTo("Drawable", circle));
}
test "protocols: registerParamImpl flags a same-file duplicate impl" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var diags = errors.DiagnosticList.init(alloc, "", "test.sx");
defer diags.deinit();
var l = Lowering.init(&module);
l.diagnostics = &diags;
l.current_source_file = "test.sx"; // both impls share a defining module
const pr = ProtocolResolver{ .l = &l };
_ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("IntCell"), .fields = &.{} } });
// impl Into(i64) for IntCell { ... } — a parameterised-protocol impl.
const args = [_]*Node{typeExpr(alloc, "i64")};
const conv = mk(alloc, .{ .fn_decl = .{ .name = "convert", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
const methods = [_]*Node{conv};
const ib = ast.ImplBlock{
.protocol_name = "Into",
.target_type = "IntCell",
.methods = &methods,
.protocol_type_args = &args,
};
const decl = mk(alloc, .{ .impl_block = ib });
// First registration is fine; the second (same key, same module) is a dup.
pr.registerImplBlock(&ib, false, decl);
pr.registerImplBlock(&ib, false, decl);
var dup_reported = false;
for (diags.items.items) |d| {
if (d.level == .err and std.mem.indexOf(u8, d.message, "duplicate impl 'Into'") != null) dup_reported = true;
}
try std.testing.expect(dup_reported);
}
// ── Planning (lookup-only; emission stays in Lowering) ───────────────
test "protocols: protocolMethodInfos lists the methods to materialize thunks for" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var l = Lowering.init(&module);
const pr = ProtocolResolver{ .l = &l };
const methods = [_]ast.ProtocolMethodDecl{ protoMethodReq("draw"), protoMethodReq("area") };
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods };
l.registerProtocolDecl(&pd);
// The registry knows exactly which methods getOrCreateThunks must thunk.
const infos = pr.protocolMethodInfos("Drawable").?;
try std.testing.expectEqual(@as(usize, 2), infos.len);
try std.testing.expectEqualStrings("draw", infos[0].name);
try std.testing.expectEqualStrings("area", infos[1].name);
// Unknown protocol → null (no silent empty-table default).
try std.testing.expect(pr.protocolMethodInfos("Nope") == null);
}
test "protocols: findVisibleImpls filters by transitive import visibility" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var l = Lowering.init(&module);
const pr = ProtocolResolver{ .l = &l };
const here_entry: Lowering.ParamImplEntry = .{ .methods = &.{}, .source_ty = .i64, .target_args = &.{}, .defining_module = "a.sx", .span = .{ .start = 0, .end = 0 } };
const other_entry: Lowering.ParamImplEntry = .{ .methods = &.{}, .source_ty = .i64, .target_args = &.{}, .defining_module = "b.sx", .span = .{ .start = 0, .end = 0 } };
const entries = [_]Lowering.ParamImplEntry{ here_entry, other_entry };
// No source-file context → falls open (all entries visible).
{
var out = std.ArrayList(Lowering.ParamImplEntry).empty;
defer out.deinit(alloc);
pr.findVisibleImpls(&entries, &out);
try std.testing.expectEqual(@as(usize, 2), out.items.len);
}
// From `a.sx`, which imports nothing: only the `a.sx` impl is visible.
{
var graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
graph.put("a.sx", std.StringHashMap(void).init(alloc)) catch unreachable;
l.program_index.import_graph = &graph;
l.current_source_file = "a.sx";
var out = std.ArrayList(Lowering.ParamImplEntry).empty;
defer out.deinit(alloc);
pr.findVisibleImpls(&entries, &out);
try std.testing.expectEqual(@as(usize, 1), out.items.len);
try std.testing.expectEqualStrings("a.sx", out.items[0].defining_module);
}
}
test "protocols: matchPackImpl selects a pack impl whose prefix + return match" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var l = Lowering.init(&module);
const pr = ProtocolResolver{ .l = &l };
// A pack impl source `Closure(..$args) -> void` (pack_start = 0).
const pack_src = module.types.closureTypePack(&.{}, .void, 0);
const convert = ast.FnDecl{ .name = "convert", .params = &.{}, .return_type = null, .body = emptyBody(alloc) };
const conv_methods = [_]*const ast.FnDecl{&convert};
const pack_entry: Lowering.PackParamImplEntry = .{
.methods = &conv_methods,
.source_pack_ty = pack_src,
.target_args = &.{},
.defining_module = "test.sx",
.span = .{ .start = 0, .end = 0 },
.pack_var_name = "args",
.ret_var_name = null,
};
var list = std.ArrayList(Lowering.PackParamImplEntry).empty;
list.append(alloc, pack_entry) catch unreachable;
const pack_key = "Into\x00Block";
l.param_impl_pack_map.put(pack_key, list) catch unreachable;
// A concrete `Closure() -> void` source matches (no fixed prefix, void ret).
const src = module.types.closureType(&.{}, .void);
const m = pr.matchPackImpl(src, pack_key).?;
try std.testing.expectEqualStrings("convert", m.convert_fd.name);
try std.testing.expectEqual(@as(usize, 0), m.src_params.len);
try std.testing.expectEqual(TypeId.void, m.src_ret);
// A non-closure source does not match; an unknown key does not match.
try std.testing.expect(pr.matchPackImpl(.i64, pack_key) == null);
try std.testing.expect(pr.matchPackImpl(src, "Into\x00Nope") == null);
}