diff --git a/src/sema.zig b/src/sema.zig index 2a06d73..2d8f742 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -46,6 +46,7 @@ pub const FnSignature = struct { pub const StructTypeInfo = struct { field_names: []const []const u8, field_types: []const Type, + type_params: []const []const u8 = &.{}, }; pub const TypeMap = std.AutoHashMap(*const Node, Type); @@ -207,6 +208,11 @@ pub const Analyzer = struct { }, .struct_decl => |sd| { try self.addSymbol(sd.name, .struct_type, .{ .struct_type = sd.name }, node.span); + var tp_names = std.ArrayList([]const u8).empty; + for (sd.type_params) |p| { + try tp_names.append(self.allocator, p.name); + } + const tp_slice = try tp_names.toOwnedSlice(self.allocator); // Populate struct_types registry, expanding #using entries if (sd.using_entries.len > 0) { var all_names = std.ArrayList([]const u8).empty; @@ -227,23 +233,25 @@ pub const Analyzer = struct { } if (i < sd.field_names.len) { try all_names.append(self.allocator, sd.field_names[i]); - const resolved = Type.fromTypeExpr(sd.field_types[i]) orelse Type.unresolved; + const resolved = self.fieldType(sd.field_types[i]); try all_types.append(self.allocator, resolved); } } try self.struct_types.put(sd.name, .{ .field_names = try all_names.toOwnedSlice(self.allocator), .field_types = try all_types.toOwnedSlice(self.allocator), + .type_params = tp_slice, }); } else { var field_types = std.ArrayList(Type).empty; for (sd.field_types) |ft| { - const resolved = Type.fromTypeExpr(ft) orelse Type.unresolved; + const resolved = self.fieldType(ft); try field_types.append(self.allocator, resolved); } try self.struct_types.put(sd.name, .{ .field_names = sd.field_names, .field_types = try field_types.toOwnedSlice(self.allocator), + .type_params = tp_slice, }); } }, @@ -344,6 +352,117 @@ pub const Analyzer = struct { return .void_type; } + /// Resolve a bare type-name string against the registry (aliases, enums, + /// structs), falling back to primitive spellings. Unlike `Type.fromName`, + /// this knows user-defined types; returns `unresolved` when it can't place + /// the name. + fn resolveTypeNameStr(self: *Analyzer, name: []const u8) Type { + if (Type.fromName(name)) |t| return t; + if (self.type_aliases.get(name)) |target| { + if (Type.fromName(target)) |t| return t; + if (self.struct_types.contains(target)) return .{ .struct_type = target }; + if (self.enum_types.contains(target)) return .{ .enum_type = target }; + } + if (self.enum_types.contains(name)) return .{ .enum_type = name }; + if (self.struct_types.contains(name)) return .{ .struct_type = name }; + return Type.unresolved; + } + + /// Extract the element/pointee name from a type-expr node, keeping generic + /// param names (`T`) intact for later substitution. Compound shapes fall + /// back to their spelled form. + fn typeExprName(self: *Analyzer, node: *Node) []const u8 { + return switch (node.data) { + .type_expr => |te| te.name, + .identifier => |id| id.name, + else => (self.resolveTypeNode(node)).displayName(self.allocator) catch "", + }; + } + + /// Resolve a struct field's declared type, preserving the raw element/ + /// pointee name of pointer/slice shapes so generic params (`T`) survive + /// into `instantiateGeneric`'s substitution. Bare names resolve through the + /// registry; the element name is resolved lazily at index/field time. + fn fieldType(self: *Analyzer, node: *Node) Type { + return switch (node.data) { + .type_expr => |te| self.resolveTypeNameStr(te.name), + .identifier => |id| self.resolveTypeNameStr(id.name), + .many_pointer_type_expr => |mp| .{ .many_pointer_type = .{ .element_name = self.typeExprName(mp.element_type) } }, + .pointer_type_expr => |p| .{ .pointer_type = .{ .pointee_name = self.typeExprName(p.pointee_type) } }, + .slice_type_expr => |s| .{ .slice_type = .{ .element_name = self.typeExprName(s.element_type) } }, + else => self.resolveTypeNode(node), + }; + } + + /// The type name an instantiation arg node carries (`Move` in + /// `List(Move)`). Null for non-nameable args (e.g. value params like `3`). + fn argTypeName(node: *const Node) ?[]const u8 { + return switch (node.data) { + .type_expr => |te| te.name, + .identifier => |id| id.name, + else => null, + }; + } + + /// Swap a single type name through a param→arg map (`T` → `Move`). + fn substName(name: []const u8, params: []const []const u8, args: []const []const u8) []const u8 { + for (params, 0..) |p, i| { + if (i < args.len and std.mem.eql(u8, p, name)) return args[i]; + } + return name; + } + + /// Substitute generic params in an already-resolved field type. Only the + /// name-carrying shapes need rewriting; the rest pass through. + fn substType(ty: Type, params: []const []const u8, args: []const []const u8) Type { + return switch (ty) { + .many_pointer_type => |i| .{ .many_pointer_type = .{ .element_name = substName(i.element_name, params, args) } }, + .slice_type => |i| .{ .slice_type = .{ .element_name = substName(i.element_name, params, args) } }, + .array_type => |i| .{ .array_type = .{ .length = i.length, .element_name = substName(i.element_name, params, args) } }, + .pointer_type => |i| .{ .pointer_type = .{ .pointee_name = substName(i.pointee_name, params, args) } }, + .struct_type => |n| .{ .struct_type = substName(n, params, args) }, + else => ty, + }; + } + + /// Instantiate `base(args...)` as a monomorphized struct entry so field + /// access resolves the generic params (`List(Move).items` → `[*]Move`). + /// Returns the instance type, or null when `base` isn't a generic struct, + /// the arity mismatches, or an arg isn't nameable. + fn instantiateGeneric(self: *Analyzer, base: []const u8, arg_nodes: []const *Node) ?Type { + const info = self.struct_types.get(base) orelse return null; + if (info.type_params.len == 0 or arg_nodes.len != info.type_params.len) return null; + + var args = std.ArrayList([]const u8).empty; + for (arg_nodes) |an| { + const nm = argTypeName(an) orelse return null; + args.append(self.allocator, nm) catch return null; + } + + // Mangle "base(A,B)". + var name_buf = std.ArrayList(u8).empty; + name_buf.appendSlice(self.allocator, base) catch return null; + name_buf.append(self.allocator, '(') catch return null; + for (args.items, 0..) |a, i| { + if (i > 0) name_buf.append(self.allocator, ',') catch return null; + name_buf.appendSlice(self.allocator, a) catch return null; + } + name_buf.append(self.allocator, ')') catch return null; + const mangled = name_buf.toOwnedSlice(self.allocator) catch return null; + + if (self.struct_types.contains(mangled)) return .{ .struct_type = mangled }; + + var new_fts = std.ArrayList(Type).empty; + for (info.field_types) |ft| { + new_fts.append(self.allocator, substType(ft, info.type_params, args.items)) catch return null; + } + self.struct_types.put(mangled, .{ + .field_names = info.field_names, + .field_types = new_fts.toOwnedSlice(self.allocator) catch return null, + }) catch return null; + return .{ .struct_type = mangled }; + } + /// Infer the type of an expression node without LLVM. /// Uses fn_signatures for call return types, struct_types for field access, /// symbols for identifier types, and Type.widen for arithmetic promotion. @@ -438,15 +557,16 @@ pub const Analyzer = struct { .index_expr => |ie| { const obj_ty = self.inferExprType(ie.object); if (obj_ty == .string_type) return Type.u(8); - if (obj_ty.isArray()) { - return Type.fromName(obj_ty.array_type.element_name) orelse Type.unresolved; - } + if (obj_ty.isArray()) return self.resolveTypeNameStr(obj_ty.array_type.element_name); + if (obj_ty.isManyPointer()) return self.resolveTypeNameStr(obj_ty.many_pointer_type.element_name); + if (obj_ty.isSlice()) return self.resolveTypeNameStr(obj_ty.slice_type.element_name); return Type.unresolved; }, .slice_expr => |se| { const obj_ty = self.inferExprType(se.object); if (obj_ty == .string_type) return .string_type; if (obj_ty.isArray()) return .{ .slice_type = .{ .element_name = obj_ty.array_type.element_name } }; + if (obj_ty.isManyPointer()) return .{ .slice_type = .{ .element_name = obj_ty.many_pointer_type.element_name } }; if (obj_ty.isSlice()) return obj_ty; return .void_type; }, @@ -466,6 +586,7 @@ pub const Analyzer = struct { // Handle parameterized struct: List(s32).{} parses as call node if (te.data == .call) { if (self.resolveCalleeName(te.data.call)) |callee| { + if (self.instantiateGeneric(callee, te.data.call.args)) |inst| return inst; if (self.struct_types.contains(callee)) return .{ .struct_type = callee }; } } @@ -492,6 +613,7 @@ pub const Analyzer = struct { .array_literal => .void_type, .type_expr => |te| .{ .meta_type = .{ .name = te.name } }, .parameterized_type_expr => |pte| { + if (self.instantiateGeneric(pte.name, pte.args)) |inst| return inst; if (self.struct_types.contains(pte.name)) return .{ .struct_type = pte.name }; return .void_type; }, @@ -1634,6 +1756,36 @@ test "sema: var_decl infers struct type from parameterized call literal" { try std.testing.expect(found_list); } +test "sema: index into generic List(T).items resolves the element struct" { + const parser_mod = @import("parser.zig"); + + const source = + "Move :: struct { score: s64; }" ++ + "List :: struct ($T: Type) { items: [*]T = null; len: s64; }" ++ + "main :: () { legal := List(Move).{}; m := legal.items[0]; }"; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var parser = parser_mod.Parser.init(alloc, source); + const root = try parser.parse(); + + var analyzer = Analyzer.init(alloc); + const result = try analyzer.analyze(root); + + var found_m = false; + for (result.symbols) |sym| { + if (std.mem.eql(u8, sym.name, "m")) { + found_m = true; + const ty = sym.ty orelse return error.TestUnexpectedResult; + try std.testing.expect(ty == .struct_type); + try std.testing.expectEqualStrings("Move", ty.struct_type); + break; + } + } + try std.testing.expect(found_m); +} + test "sema: variable shadowing in same scope is allowed" { const parser_mod = @import("parser.zig");