test(ir): lock generic substitution + mono keys before A4.1 extraction (A4.1 scaffolding step 1)

Test-first scaffolding ahead of extracting src/ir/generics.zig — no code change
to the refactor targets (buildTypeBindings / mangleGenericName / monomorphize* /
inferGenericReturnType / mangleTypeName).

Adds the first non-FFI generic/pack .ir snapshots (closing the ARCH-SAFETY §3
gap for this phase), each captured surgically via `sx ir | normalize_ir`,
path-free and idempotent:
- 0200-generics-generic            generic fn, type-param inference + mono
- 0201-generics-generic-struct     generic struct instantiation
- 0507-packs-pack-mono-dedup       mono-key dedup (same shape => one mono)
- 0518-packs-pack-value-dispatch   pack value dispatch (monomorphizePackFn)
- 0524-packs-generic-fn-pack-state-leak  pack-state isolation (issue-0048/0050
                                         class; guards the future scoped-env change)

Adds 2 unit tests via the existing public surface (no new pub exposure,
mirroring the A3.2 sub-step-1 cadence):
- mangleTypeName: pins the mono-key fragment encoding per type shape
  (s64 / ptr_X / opt_X / SL_X / mptr_X / AR_n_X / vec_n_X / struct-name / tu_X_Y).
- inferGenericReturnType: explicit type-arg path binds $T and resolves the
  -> T return (pair(s64,..) => s64, pair(f64,..) => f64).

The internal substitution/mono-key unit tests (comptime-value mangle,
buildTypeBindings strategies, scoped-env isolation) land with the generics.zig
extraction in sub-step 2, as A3.2's plan-object tests landed with CallPlan.

zig build, zig build test, tests/run_examples.sh (357/0) all green.
This commit is contained in:
agra
2026-06-02 21:05:33 +03:00
parent 1007e23561
commit 91e99f80c7
6 changed files with 18872 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -828,3 +828,92 @@ test "E1.4c noreturn typing: divergence shapes + if-else unification + block pro
defer alloc.destroy(both_div); defer alloc.destroy(both_div);
try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(both_div)); try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(both_div));
} }
// ── A4.1 test-first scaffolding: generic substitution + mono keys ────
// Lock the CURRENT behavior of the generic mono-key building blocks
// (`mangleTypeName`) and generic-return substitution (`inferGenericReturnType`)
// before they move to `src/ir/generics.zig`. Reached through the existing
// public surface — no new exposure (mirrors the A3.2 sub-step-1 cadence).
test "generics: mangleTypeName encodes the mono-key fragment per type shape" {
// Arena: the compound arms allocate fragment strings via the module
// allocator (`allocPrint` / ArrayList) and never free them — the real
// compiler runs in the compile arena, so an arena keeps the leak checker
// clean without changing the encoding under test.
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 tt = &module.types;
// Builtins — the leaf fragments `mangleGenericName` concatenates per
// bound type param (`base__<frag>...`).
try std.testing.expectEqualStrings("s64", l.mangleTypeName(.s64));
try std.testing.expectEqualStrings("u8", l.mangleTypeName(.u8));
try std.testing.expectEqualStrings("f32", l.mangleTypeName(.f32));
try std.testing.expectEqualStrings("bool", l.mangleTypeName(.bool));
try std.testing.expectEqualStrings("Any", l.mangleTypeName(.any));
try std.testing.expectEqualStrings("string", l.mangleTypeName(.string));
// Compound shapes — prefix + recursive inner fragment.
try std.testing.expectEqualStrings("ptr_s64", l.mangleTypeName(tt.ptrTo(.s64)));
try std.testing.expectEqualStrings("opt_s64", l.mangleTypeName(tt.optionalOf(.s64)));
try std.testing.expectEqualStrings("ptr_opt_u8", l.mangleTypeName(tt.ptrTo(tt.optionalOf(.u8))));
try std.testing.expectEqualStrings("SL_f64", l.mangleTypeName(tt.intern(.{ .slice = .{ .element = .f64 } })));
try std.testing.expectEqualStrings("mptr_u8", l.mangleTypeName(tt.intern(.{ .many_pointer = .{ .element = .u8 } })));
try std.testing.expectEqualStrings("AR_4_s32", l.mangleTypeName(tt.intern(.{ .array = .{ .element = .s32, .length = 4 } })));
try std.testing.expectEqualStrings("vec_3_f32", l.mangleTypeName(tt.intern(.{ .vector = .{ .element = .f32, .length = 3 } })));
// Named aggregate → its declared name.
const pt = tt.intern(.{ .@"struct" = .{ .name = tt.internString("Point"), .fields = &.{} } });
try std.testing.expectEqualStrings("Point", l.mangleTypeName(pt));
// Tuple: "tu" + "_<frag>" per field.
const tup = tt.intern(.{ .tuple = .{ .fields = &[_]TypeId{ .s64, .bool }, .names = null } });
try std.testing.expectEqualStrings("tu_s64_bool", l.mangleTypeName(tup));
}
test "generics: inferGenericReturnType binds explicit type args, resolves return" {
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);
// pair :: ($T: Type, a: T, b: T) -> T — the return type IS the bound `T`.
const tps = [_]ast.StructTypeParam{.{ .name = "T", .constraint = typeKeyword(alloc, "Type") }};
const params = [_]ast.Param{
.{ .name = "T", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeKeyword(alloc, "Type") },
.{ .name = "a", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeKeyword(alloc, "T") },
.{ .name = "b", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeKeyword(alloc, "T") },
};
const body = alloc.create(Node) catch unreachable;
body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{} } } };
const fd = ast.FnDecl{ .name = "pair", .params = &params, .return_type = typeKeyword(alloc, "T"), .body = body, .type_params = &tps };
// Explicit type arg in position 0 binds `T`; the inferred return follows it.
const mkCall = struct {
fn f(a: std.mem.Allocator, type_name: []const u8) ast.Call {
const targ = typeKeyword(a, type_name);
const x = a.create(Node) catch unreachable;
x.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 1 } } };
const y = a.create(Node) catch unreachable;
y.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 2 } } };
const callee = a.create(Node) catch unreachable;
callee.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "pair" } } };
const args = a.alloc(*Node, 3) catch unreachable;
args[0] = targ;
args[1] = x;
args[2] = y;
return .{ .callee = callee, .args = args };
}
}.f;
const c_s64 = mkCall(alloc, "s64");
try std.testing.expectEqual(TypeId.s64, l.inferGenericReturnType(&fd, &c_s64));
const c_f64 = mkCall(alloc, "f64");
try std.testing.expectEqual(TypeId.f64, l.inferGenericReturnType(&fd, &c_f64));
}