Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/check/test/cross_module_mono_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,82 @@ test "cross-module mono: static dispatch with chained method calls" {
try testing.expect(main_ident != null);
}

test "cross-module mono: recursive nominal type with self-referencing children" {
// Module A defines a recursive nominal type where children reference
// the type itself (Elem contains List(Elem)). This pattern is the key
// scenario where `remapMonotypeBetweenModules` in MIR Lower.zig does
// meaningful work: the TypeId for List(Elem) may contain a `.rec`
// placeholder indirection internally, which must be resolved when used
// from a different module for TypeId comparisons to succeed.
const source_a =
\\Elem := [Div(List(Elem)), Text(Str)].{
\\ div : List(Elem) -> Elem
\\ div = |children| Div(children)
\\
\\ text : Str -> Elem
\\ text = |content| Text(content)
\\}
;
var env_a = try MonoTestEnv.init("Elem", source_a);
defer env_a.deinit();

// Module B imports Elem and uses both constructors
const source_b =
\\import Elem
\\
\\main : Elem
\\main = Elem.div([Elem.text("hello")])
;
var env_b = try MonoTestEnv.initWithImport("B", source_b, "Elem", &env_a);
defer env_b.deinit();

// Type-check should succeed — the recursive nominal type is usable cross-module
const main_ident = env_b.module_env.common.findIdent("main");
try testing.expect(main_ident != null);
}

test "cross-module mono: recursive nominal through 3-module chain" {
// Recursive nominal type used transitively: A defines the type,
// B wraps it, C uses B's wrapper. This exercises cross-module
// TypeId canonicalization across multiple module boundaries.
const source_a =
\\Tree := [Leaf(U64), Branch(List(Tree))].{
\\ leaf : U64 -> Tree
\\ leaf = |n| Leaf(n)
\\
\\ branch : List(Tree) -> Tree
\\ branch = |children| Branch(children)
\\}
;
var env_a = try MonoTestEnv.init("Tree", source_a);
defer env_a.deinit();

const source_b =
\\import Tree
\\
\\make_pair : U64, U64 -> Tree
\\make_pair = |a, b| Tree.branch([Tree.leaf(a), Tree.leaf(b)])
;
var env_b = try MonoTestEnv.initWithImport("B", source_b, "Tree", &env_a);
defer env_b.deinit();

const source_c =
\\import B
\\import Tree
\\
\\main : Tree
\\main = B.make_pair(1, 2)
;
var env_c = try MonoTestEnv.initWithImports("C", source_c, &.{
.{ .name = "B", .env = &env_b },
.{ .name = "Tree", .env = &env_a },
});
defer env_c.deinit();

const main_ident = env_c.module_env.common.findIdent("main");
try testing.expect(main_ident != null);
}

test "type checker catches polymorphic recursion (infinite type)" {
// This test verifies that polymorphic recursion (f = |x| f([x])) is caught
// during type checking as a circular/infinite type.
Expand Down
6 changes: 3 additions & 3 deletions src/cli/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4349,12 +4349,12 @@ fn rocBuildNative(ctx: *CliContext, args: cli_args.BuildArgs) !void {
const arg_vars = platform_types.sliceVars(func.args);
if (arg_vars.len == 0) {
const func_mono_idx = mir_store.typeOf(mir_expr_id);
const func_mono = mir_store.monotype_store.getMonotype(func_mono_idx);
if (func_mono == .func) {
const resolved_func = mir_store.monotype_store.resolve(func_mono_idx);
if (resolved_func.kind == .func) {
mir_expr_id = try mir_store.addExpr(ctx.gpa, .{ .call = .{
.func = mir_expr_id,
.args = MIR.ExprSpan.empty(),
} }, func_mono.func.ret, base.Region.zero());
} }, mir_store.monotype_store.funcRet(resolved_func), base.Region.zero());
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/eval/dev_evaluator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -893,12 +893,12 @@ pub const DevEvaluator = struct {
// as calls, not as first-class function values.
if (arg_layouts.len == 0) {
const func_mono_idx = mir_store.typeOf(mir_expr_id);
const func_mono = mir_store.monotype_store.getMonotype(func_mono_idx);
if (func_mono == .func) {
const resolved_func = mir_store.monotype_store.resolve(func_mono_idx);
if (resolved_func.kind == .func) {
mir_expr_id = mir_store.addExpr(self.allocator, .{ .call = .{
.func = mir_expr_id,
.args = MIR.ExprSpan.empty(),
} }, func_mono.func.ret, base.Region.zero()) catch return error.OutOfMemory;
} }, mir_store.monotype_store.funcRet(resolved_func), base.Region.zero()) catch return error.OutOfMemory;
}
}

Expand Down
100 changes: 40 additions & 60 deletions src/layout/mir_monotype_resolver.zig
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub const Resolver = struct {
overrides: ?*const std.AutoHashMap(u32, layout.Idx),
) Allocator.Error!layout.Idx {
std.debug.assert(!mono_idx.isNone());
const key = @intFromEnum(mono_idx);
const key: u32 = @bitCast(mono_idx);

if (overrides) |override_map| {
if (override_map.get(key)) |layout_idx| return layout_idx;
Expand Down Expand Up @@ -99,71 +99,71 @@ pub const Resolver = struct {
graph: *LayoutGraph,
refs_by_mono: *std.AutoHashMap(u32, GraphRef),
) Allocator.Error!GraphRef {
const mono_key = @intFromEnum(mono_idx);
const mono_key: u32 = @bitCast(mono_idx);
if (overrides) |override_map| {
if (override_map.get(mono_key)) |layout_idx| {
return .{ .canonical = layout_idx };
}
}
if (refs_by_mono.get(mono_key)) |cached| return cached;

const mono = self.monotype_store.getMonotype(mono_idx);
const resolved_ref: GraphRef = switch (mono) {
.recursive_placeholder => unreachable,
.unit => GraphRef{ .canonical = .zst },
.prim => |p| GraphRef{ .canonical = switch (p) {
.str => layout.Idx.str,
.u8 => layout.Idx.u8,
.i8 => layout.Idx.i8,
.u16 => layout.Idx.u16,
.i16 => layout.Idx.i16,
.u32 => layout.Idx.u32,
.i32 => layout.Idx.i32,
.u64 => layout.Idx.u64,
.i64 => layout.Idx.i64,
.u128 => layout.Idx.u128,
.i128 => layout.Idx.i128,
.f32 => layout.Idx.f32,
.f64 => layout.Idx.f64,
.dec => layout.Idx.dec,
} },
const resolved_id = self.monotype_store.resolve(mono_idx);
const resolved_ref: GraphRef = switch (resolved_id.kind) {
.builtin => blk: {
if (resolved_id.builtinPrim()) |p| {
break :blk GraphRef{ .canonical = switch (p) {
.str => layout.Idx.str,
.u8 => layout.Idx.u8,
.i8 => layout.Idx.i8,
.u16 => layout.Idx.u16,
.i16 => layout.Idx.i16,
.u32 => layout.Idx.u32,
.i32 => layout.Idx.i32,
.u64 => layout.Idx.u64,
.i64 => layout.Idx.i64,
.u128 => layout.Idx.u128,
.i128 => layout.Idx.i128,
.f32 => layout.Idx.f32,
.f64 => layout.Idx.f64,
.dec => layout.Idx.dec,
} };
}
// unit
break :blk GraphRef{ .canonical = .zst };
},
.func => blk: {
const empty_captures = try self.layout_store.getEmptyRecordLayout();
break :blk GraphRef{ .canonical = try self.layout_store.insertLayout(layout.Layout.closure(empty_captures)) };
},
.box => |b| blk: {
.box => blk: {
const node_id = try graph.reserveNode(self.allocator);
const local_ref = GraphRef{ .local = node_id };
try refs_by_mono.put(mono_key, local_ref);
const child_ref = try self.buildRefForMonotype(b.inner, overrides, graph, refs_by_mono);
const child_ref = try self.buildRefForMonotype(self.monotype_store.boxInner(resolved_id), overrides, graph, refs_by_mono);
graph.setNode(node_id, .{ .box = child_ref });
break :blk local_ref;
},
.list => |l| blk: {
.list => blk: {
const node_id = try graph.reserveNode(self.allocator);
const local_ref = GraphRef{ .local = node_id };
try refs_by_mono.put(mono_key, local_ref);
const child_ref = try self.buildRefForMonotype(l.elem, overrides, graph, refs_by_mono);
const child_ref = try self.buildRefForMonotype(self.monotype_store.listElem(resolved_id), overrides, graph, refs_by_mono);
graph.setNode(node_id, .{ .list = child_ref });
break :blk local_ref;
},
.record => |r| try self.buildStructFromFields(self.monotype_store.getFields(r.fields), overrides, graph, refs_by_mono),
.tuple => |t| try self.buildStructFromElems(self.monotype_store.getIdxSpan(t.elems), overrides, graph, refs_by_mono),
.tag_union => |tu| try self.buildTagUnionRef(self.monotype_store.getTags(tu.tags), overrides, graph, refs_by_mono),
.record => try self.buildStructFromFields(self.monotype_store.recordFields(resolved_id), overrides, graph, refs_by_mono),
.tuple => try self.buildStructFromElems(self.monotype_store.tupleElems(resolved_id), overrides, graph, refs_by_mono),
.tag_union => try self.buildTagUnionRef(self.monotype_store.tagUnionTags(resolved_id), overrides, graph, refs_by_mono),
.rec => unreachable, // already resolved above
};

if (findEquivalentMonotypeRef(self.monotype_store, mono_idx, refs_by_mono, mono_key)) |equivalent| {
try refs_by_mono.put(mono_key, equivalent);
return equivalent;
}

try refs_by_mono.put(mono_key, resolved_ref);
return resolved_ref;
}

fn buildStructFromElems(
self: *Resolver,
elems: []const Monotype.Idx,
elems: []const Monotype.TypeId,
overrides: ?*const std.AutoHashMap(u32, layout.Idx),
graph: *LayoutGraph,
refs_by_mono: *std.AutoHashMap(u32, GraphRef),
Expand All @@ -184,7 +184,7 @@ pub const Resolver = struct {

fn buildStructFromFields(
self: *Resolver,
fields_slice: []const Monotype.Field,
fields_slice: []const Monotype.FieldKey,
overrides: ?*const std.AutoHashMap(u32, layout.Idx),
graph: *LayoutGraph,
refs_by_mono: *std.AutoHashMap(u32, GraphRef),
Expand All @@ -197,7 +197,7 @@ pub const Resolver = struct {
for (fields_slice, 0..) |field, i| {
fields.appendAssumeCapacity(.{
.index = @intCast(i),
.child = try self.buildRefForMonotype(field.type_idx, overrides, graph, refs_by_mono),
.child = try self.buildRefForMonotype(field.ty, overrides, graph, refs_by_mono),
});
}
return self.buildStructNode(fields.items, graph);
Expand All @@ -218,7 +218,7 @@ pub const Resolver = struct {

fn buildTagUnionRef(
self: *Resolver,
tags: []const Monotype.Tag,
tags: []const Monotype.TagKey,
overrides: ?*const std.AutoHashMap(u32, layout.Idx),
graph: *LayoutGraph,
refs_by_mono: *std.AutoHashMap(u32, GraphRef),
Expand All @@ -230,7 +230,7 @@ pub const Resolver = struct {
try variants.ensureTotalCapacity(self.allocator, tags.len);
for (tags) |tag| {
variants.appendAssumeCapacity(try self.buildPayloadRef(
self.monotype_store.getIdxSpan(tag.payloads),
self.monotype_store.getIdxListItems(tag.payloads),
overrides,
graph,
refs_by_mono,
Expand All @@ -245,7 +245,7 @@ pub const Resolver = struct {

fn buildPayloadRef(
self: *Resolver,
payloads: []const Monotype.Idx,
payloads: []const Monotype.TypeId,
overrides: ?*const std.AutoHashMap(u32, layout.Idx),
graph: *LayoutGraph,
refs_by_mono: *std.AutoHashMap(u32, GraphRef),
Expand All @@ -255,26 +255,6 @@ pub const Resolver = struct {
}
};

fn findEquivalentMonotypeRef(
monotype_store: *const Monotype.Store,
mono_idx: Monotype.Idx,
refs_by_mono: *const std.AutoHashMap(u32, GraphRef),
mono_key: u32,
) ?GraphRef {
const mono = monotype_store.getMonotype(mono_idx);

var iter = refs_by_mono.iterator();
while (iter.next()) |entry| {
if (entry.key_ptr.* == mono_key) continue;
const other_idx: Monotype.Idx = @enumFromInt(entry.key_ptr.*);
if (std.meta.eql(monotype_store.getMonotype(other_idx), mono)) {
return entry.value_ptr.*;
}
}

return null;
}

fn findEquivalentRootNode(
allocator: Allocator,
graph: *const LayoutGraph,
Expand Down
5 changes: 1 addition & 4 deletions src/layout/store_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,21 @@ fn expectTypeAndMonotypeResolversAgree(
const type_layout_idx = try type_layout_resolver.resolve(0, type_var, &lt.type_scope, null);

var mono_store = try mir.Monotype.Store.init(allocator);
defer mono_store.deinit(allocator);
defer mono_store.deinit();

var scratches = try mir.Monotype.Store.Scratches.init(allocator);
defer scratches.deinit();
scratches.ident_store = lt.module_env.getIdentStoreConst();

var specializations = std.AutoHashMap(types.Var, mir.Monotype.Idx).init(allocator);
defer specializations.deinit();
var nominal_cycle_breakers = std.AutoHashMap(types.Var, mir.Monotype.Idx).init(allocator);
defer nominal_cycle_breakers.deinit();

const mono_idx = try mono_store.fromTypeVar(
allocator,
&lt.type_store,
type_var,
lt.module_env.idents,
&specializations,
&nominal_cycle_breakers,
&scratches,
);

Expand Down
Loading
Loading