diff --git a/src/ir/ir.zig b/src/ir/ir.zig index fa24b05..a594a98 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -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"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 416abde..c0c9d33 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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: `$[]` 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); }, diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 9594b73..06bd203 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -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); diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig new file mode 100644 index 0000000..e911bf0 --- /dev/null +++ b/src/ir/type_resolver.test.zig @@ -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); +} diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig new file mode 100644 index 0000000..a292560 --- /dev/null +++ b/src/ir/type_resolver.zig @@ -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, + }; + } +};