refactor(ir): add ResolveEnv + TypeResolver shell; own primitives + compounds (A2.1)

Architecture phase A2.1 -- behavior-preserving. Introduce src/ir/type_resolver.zig
as the canonical AST-type-node -> TypeId resolver (Principle 1), starting with:

- ResolveEnv: the explicit resolution-context shape (Principle 2) -- type/pack/
  comptime bindings + target_type. Defined now; consumed as A2.2/A2.3 move the
  cases that need it.
- TypeResolver.resolvePrimitive(name): the builtin keyword table, MOVED here from
  type_bridge.resolveTypePrimitive (now a re-export -> single source; its 7
  callers are unaffected; no import cycle).
- TypeResolver.resolveCompound(node, inner): the structural compound types
  *T / [*]T / []T / ?T / [N]T. Element types recurse via inner.resolveInner (an
  anytype callback) so generic structs / bindings in element position keep their
  full stateful resolution.

Lowering.resolveTypeWithBindings duplicated the 5 simple compounds across its
bindings and no-bindings blocks (10 arms). Both are replaced with a single
self.typeResolver().resolveCompound(node, self) delegation; adds
Lowering.resolveInner (recursion hook) + typeResolver() (by-value view).

Deliberately deferred: tuples, closures, and function types stay on the existing
pack-aware helpers (resolveClosure/Tuple/FunctionTypeWithBindings); A2.3 owns
their pack-projection logic.

Tests: src/ir/type_resolver.test.zig (resolvePrimitive keyword/null cases;
resolveCompound for all 5 + null for non-compound; ResolveEnv defaults), wired
into the ir.zig barrel.

No new fallback path; no duplicate truth. Gate green: zig build, zig build test,
bash tests/run_examples.sh (350 passed, 0 failed). lower.zig 19393 -> 19372.
This commit is contained in:
agra
2026-06-02 13:25:27 +03:00
parent 8fbaf9ca6a
commit 9eb85cf9e3
5 changed files with 213 additions and 79 deletions

View File

@@ -5,6 +5,7 @@ pub const print = @import("print.zig");
pub const interp = @import("interp.zig");
pub const lower = @import("lower.zig");
pub const program_index = @import("program_index.zig");
pub const type_resolver = @import("type_resolver.zig");
pub const TypeId = types.TypeId;
pub const TypeInfo = types.TypeInfo;
@@ -32,6 +33,8 @@ pub const Interpreter = interp.Interpreter;
pub const Value = interp.Value;
pub const Lowering = lower.Lowering;
pub const ProgramIndex = program_index.ProgramIndex;
pub const TypeResolver = type_resolver.TypeResolver;
pub const ResolveEnv = type_resolver.ResolveEnv;
pub const compiler_hooks = @import("compiler_hooks.zig");
pub const emit_llvm = @import("emit_llvm.zig");
@@ -51,6 +54,7 @@ pub const print_tests = @import("print.test.zig");
pub const interp_tests = @import("interp.test.zig");
pub const lower_tests = @import("lower.test.zig");
pub const program_index_tests = @import("program_index.test.zig");
pub const type_resolver_tests = @import("type_resolver.test.zig");
pub const type_bridge_tests = @import("type_bridge.test.zig");
pub const emit_llvm_tests = @import("emit_llvm.test.zig");
pub const jni_descriptor_tests = @import("jni_descriptor.test.zig");

View File

@@ -19,6 +19,7 @@ const TemplateParam = program_index_mod.TemplateParam;
const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo;
const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo;
const ModuleConstInfo = program_index_mod.ModuleConstInfo;
const TypeResolver = @import("type_resolver.zig").TypeResolver;
const TypeId = types.TypeId;
const StringId = types.StringId;
@@ -12730,6 +12731,24 @@ pub const Lowering = struct {
return self.resolveTypeWithBindings(type_ann);
}
/// Construct a `TypeResolver` view over the current lowering state (borrows
/// only; cheap by-value, reflects current `diagnostics` / `program_index`).
fn typeResolver(self: *Lowering) TypeResolver {
return .{
.alloc = self.alloc,
.types = &self.module.types,
.diagnostics = self.diagnostics,
.index = &self.program_index,
};
}
/// Inner-type recursion hook for `TypeResolver.resolveCompound`: resolves a
/// child type node through the full stateful resolver, so generic structs /
/// bindings / aliases in element position keep their resolution.
pub fn resolveInner(self: *Lowering, node: *const Node) TypeId {
return self.resolveTypeWithBindings(node);
}
/// Resolve a type node, checking type_bindings first for generic type params.
fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId {
// Pack-index in a type position: `$<pack>[<lit>]` resolves to the
@@ -12774,6 +12793,11 @@ pub const Lowering = struct {
}
}
}
// Structural compound types (`*T`, `[*]T`, `[]T`, `?T`, `[N]T`) are
// owned by TypeResolver (A2.1). Element types recurse through the full
// stateful resolver (`resolveInner` → here) so generic structs /
// bindings in element position keep their resolution.
if (self.typeResolver().resolveCompound(node, self)) |t| return t;
if (self.type_bindings) |tb| {
switch (node.data) {
.type_expr => |te| {
@@ -12786,31 +12810,6 @@ pub const Lowering = struct {
.identifier => |id| {
if (tb.get(id.name)) |ty| return ty;
},
// Compound types: resolve inner types with bindings
.slice_type_expr => |st| {
const elem = self.resolveTypeWithBindings(st.element_type);
return self.module.types.sliceOf(elem);
},
.pointer_type_expr => |pt| {
const pointee = self.resolveTypeWithBindings(pt.pointee_type);
return self.module.types.ptrTo(pointee);
},
.many_pointer_type_expr => |mp| {
const elem = self.resolveTypeWithBindings(mp.element_type);
return self.module.types.manyPtrTo(elem);
},
.optional_type_expr => |ot| {
const child = self.resolveTypeWithBindings(ot.inner_type);
return self.module.types.optionalOf(child);
},
.array_type_expr => |at| {
const elem = self.resolveTypeWithBindings(at.element_type);
const len: u32 = blk: {
if (at.length.data == .int_literal) break :blk @intCast(at.length.data.int_literal.value);
break :blk 0;
};
return self.module.types.arrayOf(elem, len);
},
.parameterized_type_expr => |pt| {
return self.resolveParameterizedWithBindings(&pt);
},
@@ -12834,30 +12833,10 @@ pub const Lowering = struct {
if (node.data == .call) {
return self.resolveTypeCallWithBindings(&node.data.call);
}
// Handle compound types that may contain generic structs (e.g., *List(ViewChild))
// These need the lowerer's resolveType to properly instantiate generics.
// Pointers / slices / many-pointers / optionals / arrays are owned by
// TypeResolver (handled above). The pack-aware tuple / closure /
// function shapes resolve here — A2.3 owns their pack projection logic.
switch (node.data) {
.pointer_type_expr => |pt| {
const pointee = self.resolveTypeWithBindings(pt.pointee_type);
return self.module.types.ptrTo(pointee);
},
.slice_type_expr => |st| {
const elem = self.resolveTypeWithBindings(st.element_type);
return self.module.types.sliceOf(elem);
},
.many_pointer_type_expr => |mp| {
const elem = self.resolveTypeWithBindings(mp.element_type);
return self.module.types.manyPtrTo(elem);
},
.optional_type_expr => |ot| {
const child = self.resolveTypeWithBindings(ot.inner_type);
return self.module.types.optionalOf(child);
},
.array_type_expr => |at| {
const elem = self.resolveTypeWithBindings(at.element_type);
const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0;
return self.module.types.arrayOf(elem, len);
},
.closure_type_expr => |ct| {
return self.resolveClosureTypeWithBindings(&ct);
},

View File

@@ -8,6 +8,7 @@ const TypeId = ir_types.TypeId;
const TypeInfo = ir_types.TypeInfo;
const TypeTable = ir_types.TypeTable;
const StringId = ir_types.StringId;
const type_resolver = @import("type_resolver.zig");
// ── AST Node → TypeId ───────────────────────────────────────────────────
// Resolve an AST type node into an IR TypeId. Used during lowering when
@@ -251,37 +252,11 @@ fn resolveTypeName(name: []const u8, table: *TypeTable) TypeId {
return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
}
pub fn resolveTypePrimitive(name: []const u8) ?TypeId {
if (name.len == 0) return null;
// Fast path for common types
if (std.mem.eql(u8, name, "s64")) return .s64;
if (std.mem.eql(u8, name, "s32")) return .s32;
if (std.mem.eql(u8, name, "s16")) return .s16;
if (std.mem.eql(u8, name, "s8")) return .s8;
if (std.mem.eql(u8, name, "u64")) return .u64;
if (std.mem.eql(u8, name, "u32")) return .u32;
if (std.mem.eql(u8, name, "u16")) return .u16;
if (std.mem.eql(u8, name, "u8")) return .u8;
if (std.mem.eql(u8, name, "f32")) return .f32;
if (std.mem.eql(u8, name, "f64")) return .f64;
if (std.mem.eql(u8, name, "bool")) return .bool;
if (std.mem.eql(u8, name, "string")) return .string;
if (std.mem.eql(u8, name, "void")) return .void;
if (std.mem.eql(u8, name, "Any")) return .any;
// Type values are runtime-representable as Any-shaped pairs:
// `{ tag = .any.index() (the meta-marker), value = TypeId.index() }`.
// Lets `t : Type = f64; t == s64; print(t)` all route through the
// existing Any infrastructure — boxing/unboxing, `case type:`
// dispatch, runtime `type_name(t)` via the type-name lookup
// table. Comparison decomposes via the eq fold path
// (`isStaticTypeRef`) for static literals; runtime-var vs
// literal compares decompose at `lowerBinaryOp`.
if (std.mem.eql(u8, name, "Type")) return .any;
if (std.mem.eql(u8, name, "noreturn")) return .noreturn;
if (std.mem.eql(u8, name, "usize")) return .usize;
if (std.mem.eql(u8, name, "isize")) return .isize;
return null;
}
/// Builtin primitive keyword → TypeId. The keyword table now lives in
/// `type_resolver.zig` (architecture phase A2.1, `TypeResolver.resolvePrimitive`);
/// re-exported here so existing callers are unaffected while `type_bridge` is
/// retired (A2.2). Single source of truth: the table is defined once, there.
pub const resolveTypePrimitive = type_resolver.TypeResolver.resolvePrimitive;
fn resolveArrayType(at: *const ast.ArrayTypeExpr, table: *TypeTable) TypeId {
const elem = resolveAstType(at.element_type, table);

View File

@@ -0,0 +1,86 @@
const std = @import("std");
const ast = @import("../ast.zig");
const Node = ast.Node;
const types = @import("types.zig");
const TypeTable = types.TypeTable;
const TypeId = types.TypeId;
const tr_mod = @import("type_resolver.zig");
const TypeResolver = tr_mod.TypeResolver;
const ResolveEnv = tr_mod.ResolveEnv;
const ProgramIndex = @import("program_index.zig").ProgramIndex;
fn typeExpr(name: []const u8) Node {
return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = name, .is_generic = false } } };
}
/// Stand-in for `Lowering.resolveInner`: the real hook recurses the full
/// stateful resolver; here element types are always primitives, resolved via
/// the keyword table.
const PrimInner = struct {
pub fn resolveInner(_: PrimInner, node: *const Node) TypeId {
return switch (node.data) {
.type_expr => |te| TypeResolver.resolvePrimitive(te.name) orelse .unresolved,
else => .unresolved,
};
}
};
test "TypeResolver.resolvePrimitive maps builtin keywords, null otherwise" {
try std.testing.expectEqual(@as(?TypeId, .s64), TypeResolver.resolvePrimitive("s64"));
try std.testing.expectEqual(@as(?TypeId, .bool), TypeResolver.resolvePrimitive("bool"));
try std.testing.expectEqual(@as(?TypeId, .f64), TypeResolver.resolvePrimitive("f64"));
try std.testing.expectEqual(@as(?TypeId, .void), TypeResolver.resolvePrimitive("void"));
try std.testing.expectEqual(@as(?TypeId, .any), TypeResolver.resolvePrimitive("Any"));
try std.testing.expectEqual(@as(?TypeId, .any), TypeResolver.resolvePrimitive("Type"));
try std.testing.expectEqual(@as(?TypeId, .usize), TypeResolver.resolvePrimitive("usize"));
try std.testing.expectEqual(@as(?TypeId, .isize), TypeResolver.resolvePrimitive("isize"));
try std.testing.expectEqual(@as(?TypeId, .noreturn), TypeResolver.resolvePrimitive("noreturn"));
// Non-primitives (aliases / generics / named structs) defer to the caller.
try std.testing.expect(TypeResolver.resolvePrimitive("List") == null);
try std.testing.expect(TypeResolver.resolvePrimitive("ShaderHandle") == null);
try std.testing.expect(TypeResolver.resolvePrimitive("") == null);
}
test "TypeResolver.resolveCompound builds structural compound types" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
var index = ProgramIndex.init(alloc);
defer index.deinit();
const tr = TypeResolver{ .alloc = alloc, .types = &table, .diagnostics = null, .index = &index };
const inner = PrimInner{};
var s64n = typeExpr("s64");
var u8n = typeExpr("u8");
var f32n = typeExpr("f32");
var booln = typeExpr("bool");
var s32n = typeExpr("s32");
var ptr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = &s64n } } };
try std.testing.expectEqual(@as(?TypeId, table.ptrTo(.s64)), tr.resolveCompound(&ptr, inner));
var mptr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .many_pointer_type_expr = .{ .element_type = &u8n } } };
try std.testing.expectEqual(@as(?TypeId, table.manyPtrTo(.u8)), tr.resolveCompound(&mptr, inner));
var slice = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .slice_type_expr = .{ .element_type = &f32n } } };
try std.testing.expectEqual(@as(?TypeId, table.sliceOf(.f32)), tr.resolveCompound(&slice, inner));
var opt = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .optional_type_expr = .{ .inner_type = &booln } } };
try std.testing.expectEqual(@as(?TypeId, table.optionalOf(.bool)), tr.resolveCompound(&opt, inner));
var len = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 3 } } };
var arr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .array_type_expr = .{ .length = &len, .element_type = &s32n } } };
try std.testing.expectEqual(@as(?TypeId, table.arrayOf(.s32, 3)), tr.resolveCompound(&arr, inner));
// Non-compound nodes are not this resolver's responsibility → null, so the
// caller continues with name / tuple / closure / generic resolution.
var name = typeExpr("List");
try std.testing.expect(tr.resolveCompound(&name, inner) == null);
}
test "ResolveEnv default-constructs with all-null context" {
const env = ResolveEnv{};
try std.testing.expect(env.type_bindings == null);
try std.testing.expect(env.pack_bindings == null);
try std.testing.expect(env.target_type == null);
}

90
src/ir/type_resolver.zig Normal file
View File

@@ -0,0 +1,90 @@
const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const errors = @import("../errors.zig");
const program_index_mod = @import("program_index.zig");
const Node = ast.Node;
const TypeId = types.TypeId;
const TypeTable = types.TypeTable;
const ProgramIndex = program_index_mod.ProgramIndex;
/// Explicit, caller-supplied resolution context (architecture Principle 2):
/// the inputs that steer AST type-node resolution, replacing ad-hoc mutable
/// `Lowering` fields (`type_bindings`, `pack_*`, `comptime_value_bindings`,
/// `target_type`, …). A2.1 defines the shape; fields are consumed as later
/// phases move the cases that need them (generics/aliases A2.2, packs A2.3).
pub const ResolveEnv = struct {
type_bindings: ?*const std.StringHashMap(TypeId) = null,
pack_bindings: ?*const std.StringHashMap([]const TypeId) = null,
pack_arg_types: ?*const std.StringHashMap([]const TypeId) = null,
pack_constraints: ?*const std.StringHashMap([]const u8) = null,
comptime_values: ?*const std.StringHashMap(i64) = null,
target_type: ?TypeId = null,
};
/// Canonical AST-type-node → `TypeId` resolver (architecture phase A2). As of
/// A2.1 it owns the primitive-keyword table and the structural compound type
/// constructors. Later phases fold in generics/aliases (A2.2) and pack
/// projections (A2.3) and retire `src/ir/type_bridge.zig` (Principle 1).
///
/// Holds borrowed references only — constructed cheaply by value at each call
/// site (`Lowering.typeResolver()`), so it always reflects current state.
pub const TypeResolver = struct {
alloc: std.mem.Allocator,
types: *TypeTable,
diagnostics: ?*errors.DiagnosticList,
index: *ProgramIndex,
/// Builtin primitive keyword → `TypeId`; `null` for any non-primitive name
/// (the caller then continues with generic / alias / named-struct
/// resolution). Single source of truth for the builtin keyword set.
/// Namespaced (no `self`) — primitive resolution is stateless.
pub fn resolvePrimitive(name: []const u8) ?TypeId {
if (name.len == 0) return null;
if (std.mem.eql(u8, name, "s64")) return .s64;
if (std.mem.eql(u8, name, "s32")) return .s32;
if (std.mem.eql(u8, name, "s16")) return .s16;
if (std.mem.eql(u8, name, "s8")) return .s8;
if (std.mem.eql(u8, name, "u64")) return .u64;
if (std.mem.eql(u8, name, "u32")) return .u32;
if (std.mem.eql(u8, name, "u16")) return .u16;
if (std.mem.eql(u8, name, "u8")) return .u8;
if (std.mem.eql(u8, name, "f32")) return .f32;
if (std.mem.eql(u8, name, "f64")) return .f64;
if (std.mem.eql(u8, name, "bool")) return .bool;
if (std.mem.eql(u8, name, "string")) return .string;
if (std.mem.eql(u8, name, "void")) return .void;
if (std.mem.eql(u8, name, "Any")) return .any;
// `Type` values are runtime-representable as Any-shaped pairs
// `{ tag = .any.index(), value = TypeId.index() }`, so `Type` maps to
// `.any` and routes through the existing Any infrastructure.
if (std.mem.eql(u8, name, "Type")) return .any;
if (std.mem.eql(u8, name, "noreturn")) return .noreturn;
if (std.mem.eql(u8, name, "usize")) return .usize;
if (std.mem.eql(u8, name, "isize")) return .isize;
return null;
}
/// Structural compound types whose meaning is fully determined by their
/// node kind and element type(s): `*T`, `[*]T`, `[]T`, `?T`, `[N]T`.
/// Element types are resolved via `inner.resolveInner(node)` so generic
/// structs / bindings / aliases in element position keep their full
/// (caller-side, stateful) resolution. Returns `null` for any other node
/// kind — names, tuples, closures/functions (pack-aware, A2.3),
/// parameterized types, pack-index, `Self` — which the caller handles.
pub fn resolveCompound(self: TypeResolver, node: *const Node, inner: anytype) ?TypeId {
return switch (node.data) {
.pointer_type_expr => |pt| self.types.ptrTo(inner.resolveInner(pt.pointee_type)),
.many_pointer_type_expr => |mp| self.types.manyPtrTo(inner.resolveInner(mp.element_type)),
.slice_type_expr => |st| self.types.sliceOf(inner.resolveInner(st.element_type)),
.optional_type_expr => |ot| self.types.optionalOf(inner.resolveInner(ot.inner_type)),
.array_type_expr => |at| blk: {
const elem = inner.resolveInner(at.element_type);
const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0;
break :blk self.types.arrayOf(elem, len);
},
else => null,
};
}
};