From a7a1e2d5e5587a7a52ec204d9cd11e01c4636744 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Mon, 24 Aug 2026 01:12:49 +0600 Subject: [PATCH 1/2] Add generic named type instances Parse explicit type applications and multi-segment qualified paths while preserving comparison and shift parsing. Resolve declaration-owned type parameters through one syntax conversion path, cache concrete instances in CompilerContext, and purge owner instances on incremental reset. Preserve nominal enum identity and lower concrete struct and interface instances through existing HIR, MIR, and LLVM paths. Path, substitution, traversal, and cache helpers centralize shared semantic invariants; none are pass-through compatibility wrappers. Validated with full uncached tests, vet, race tests, compiler build, bundled-binary x_test, gofmt, and git diff checks. --- internal/frontend/ast/clone.go | 22 +++- internal/frontend/ast/decl.go | 34 +++++ internal/frontend/ast/expr.go | 66 +++++++--- internal/frontend/ast/node.go | 1 + internal/frontend/parser/parse_expr.go | 47 +++++-- internal/frontend/parser/parse_types.go | 93 ++++++++++++-- internal/frontend/parser/parser_test.go | 55 ++++++++ internal/ir/hir/lower/module_lower.go | 14 +- internal/lsp/cursor.go | 19 +-- internal/lsp/hover.go | 24 +++- internal/lsp/server_test.go | 16 +++ internal/pipeline/pipeline_test.go | 80 ++++++++++++ internal/project/context.go | 20 +++ internal/project/export_fingerprint.go | 13 +- internal/project/generic_types.go | 99 +++++++++++++++ internal/project/modules_test.go | 22 ++++ internal/project/type_syntax.go | 15 +++ internal/semantics/binder/binder.go | 18 +-- internal/semantics/binder/binder_test.go | 110 ++++++++++++++++ internal/semantics/binder/type_decl_cycles.go | 12 +- internal/semantics/collector/collector.go | 41 ++++-- .../semantics/collector/collector_test.go | 2 +- internal/semantics/resolver/resolver.go | 21 ++- .../semantics/typechecker/assignability.go | 6 +- internal/semantics/typechecker/check_call.go | 13 +- internal/semantics/typechecker/check_fn.go | 45 +++++-- .../semantics/typechecker/typechecker_test.go | 17 +++ internal/semantics/typeinfo/capabilities.go | 2 +- internal/semantics/typeinfo/compatibility.go | 6 + internal/semantics/typeinfo/relations.go | 39 ++++++ internal/semantics/typeinfo/syntax.go | 120 +++++++++++++----- internal/semantics/typeinfo/types.go | 117 +++++++++++++---- internal/semantics/typeinfo/types_test.go | 41 ++++++ .../negative_generic_type_arity/peeper.toml | 7 + .../negative_generic_type_arity/src/main.peep | 5 + .../peeper.toml | 7 + .../src/main.peep | 5 + .../src/runtime.peep | 3 + .../peeper.toml | 7 + .../src/main.peep | 3 + .../runtime_generic_named_types/peeper.toml | 7 + .../runtime_generic_named_types/src/main.peep | 15 +++ x_test/type_imported_generic/peeper.toml | 6 + .../type_imported_generic/src/container.peep | 3 + x_test/type_imported_generic/src/main.peep | 10 ++ 45 files changed, 1167 insertions(+), 161 deletions(-) create mode 100644 internal/project/generic_types.go create mode 100644 x_test/negative_generic_type_arity/peeper.toml create mode 100644 x_test/negative_generic_type_arity/src/main.peep create mode 100644 x_test/negative_imported_value_type_arguments/peeper.toml create mode 100644 x_test/negative_imported_value_type_arguments/src/main.peep create mode 100644 x_test/negative_imported_value_type_arguments/src/runtime.peep create mode 100644 x_test/negative_nongeneric_type_arguments/peeper.toml create mode 100644 x_test/negative_nongeneric_type_arguments/src/main.peep create mode 100644 x_test/runtime_generic_named_types/peeper.toml create mode 100644 x_test/runtime_generic_named_types/src/main.peep create mode 100644 x_test/type_imported_generic/peeper.toml create mode 100644 x_test/type_imported_generic/src/container.peep create mode 100644 x_test/type_imported_generic/src/main.peep diff --git a/internal/frontend/ast/clone.go b/internal/frontend/ast/clone.go index 213dba3b..96e26d57 100644 --- a/internal/frontend/ast/clone.go +++ b/internal/frontend/ast/clone.go @@ -56,6 +56,12 @@ func cloneTypeExpr(typ TypeExpr, newID func(NodeID, bool) NodeID, fromArgument b switch typ := typ.(type) { case *NamedType: return &NamedType{NodeIDHolder: id, Name: typ.Name, Location: typ.Location} + case *AppliedType: + args := make([]TypeExpr, len(typ.TypeArgs)) + for index, arg := range typ.TypeArgs { + args[index] = cloneTypeExpr(arg, newID, fromArgument) + } + return &AppliedType{NodeIDHolder: id, Name: cloneIdent(typ.Name, newID, fromArgument), TypeArgs: args, Location: typ.Location} case *OwnedPtrType: return &OwnedPtrType{NodeIDHolder: id, Target: cloneTypeExpr(typ.Target, newID, fromArgument), Location: typ.Location} case *RawPtrType: @@ -108,12 +114,26 @@ func cloneTypeExpr(typ TypeExpr, newID func(NodeID, bool) NodeID, fromArgument b } return &EnumType{NodeIDHolder: id, Variants: variants, Location: typ.Location} case *ScopeResolution: - return &ScopeResolution{NodeIDHolder: id, Module: cloneIdent(typ.Module, newID, fromArgument), Name: cloneIdent(typ.Name, newID, fromArgument), Location: typ.Location} + return &ScopeResolution{NodeIDHolder: id, Segments: clonePathSegments(typ.Segments, newID, fromArgument), Location: typ.Location} default: panic("unhandled type expression in call-default clone") } } +func clonePathSegments(segments []PathSegment, newID func(NodeID, bool) NodeID, fromArgument bool) []PathSegment { + cloned := make([]PathSegment, len(segments)) + for index, segment := range segments { + args := make([]TypeExpr, len(segment.TypeArgs)) + for argIndex, arg := range segment.TypeArgs { + args[argIndex] = cloneTypeExpr(arg, newID, fromArgument) + } + cloned[index] = PathSegment{ + Name: cloneIdent(segment.Name, newID, fromArgument), TypeArgs: args, Location: segment.Location, + } + } + return cloned +} + func cloneParam(param Param, newID func(NodeID, bool) NodeID, fromArgument bool) Param { cloned := Param{IsMutable: param.IsMutable, Name: cloneIdent(param.Name, newID, fromArgument), Type: cloneTypeExpr(param.Type, newID, fromArgument), Location: param.Location} if param.Default != nil { diff --git a/internal/frontend/ast/decl.go b/internal/frontend/ast/decl.go index c072db3a..081925c7 100644 --- a/internal/frontend/ast/decl.go +++ b/internal/frontend/ast/decl.go @@ -28,6 +28,28 @@ type NamedType struct { Location *source.Location } +type AppliedType struct { + NodeIDHolder + Name *Ident + TypeArgs []TypeExpr + Location *source.Location +} + +func (*AppliedType) typeNode() {} +func (t *AppliedType) forEachChild(visit func(Node)) { + visit(t.Name) + for _, arg := range t.TypeArgs { + visit(arg) + } +} +func (t *AppliedType) loc() *source.Location { return t.Location } +func (t *AppliedType) TypeText() string { + if t == nil { + return "" + } + return appliedTypeText(t.Name, t.TypeArgs) +} + func (*NamedType) typeNode() {} func (*NamedType) forEachChild(func(Node)) {} func (t *NamedType) loc() *source.Location { return t.Location } @@ -467,6 +489,9 @@ func (d *TypeAliasDecl) forEachChild(visit func(Node)) { } func (d *TypeAliasDecl) loc() *source.Location { return d.Location } func (d *TypeAliasDecl) DeclName() *Ident { return d.Name } +func (d *TypeAliasDecl) DeclarationTypeParams() []TypeParam { + return d.TypeParams +} func (d *TypeAliasDecl) UnderlyingType() TypeExpr { return d.Type } @@ -493,6 +518,9 @@ func (d *StructDecl) forEachChild(visit func(Node)) { } func (d *StructDecl) loc() *source.Location { return d.Location } func (d *StructDecl) DeclName() *Ident { return d.Name } +func (d *StructDecl) DeclarationTypeParams() []TypeParam { + return d.TypeParams +} func (d *StructDecl) UnderlyingType() TypeExpr { return d.Type } @@ -518,6 +546,9 @@ func (d *InterfaceDecl) forEachChild(visit func(Node)) { } func (d *InterfaceDecl) loc() *source.Location { return d.Location } func (d *InterfaceDecl) DeclName() *Ident { return d.Name } +func (d *InterfaceDecl) DeclarationTypeParams() []TypeParam { + return d.TypeParams +} func (d *InterfaceDecl) UnderlyingType() TypeExpr { return d.Type } @@ -543,6 +574,9 @@ func (d *EnumDecl) forEachChild(visit func(Node)) { } func (d *EnumDecl) loc() *source.Location { return d.Location } func (d *EnumDecl) DeclName() *Ident { return d.Name } +func (d *EnumDecl) DeclarationTypeParams() []TypeParam { + return d.TypeParams +} func (d *EnumDecl) UnderlyingType() TypeExpr { return d.Type } diff --git a/internal/frontend/ast/expr.go b/internal/frontend/ast/expr.go index c8eb11e0..ae787288 100644 --- a/internal/frontend/ast/expr.go +++ b/internal/frontend/ast/expr.go @@ -36,16 +36,25 @@ func (e *Ident) copyExpr(substitutions map[string]Expr, newID func(NodeID, bool) type ScopeResolution struct { NodeIDHolder - Module *Ident + Segments []PathSegment + Location *source.Location +} + +type PathSegment struct { Name *Ident + TypeArgs []TypeExpr Location *source.Location } func (*ScopeResolution) exprNode() {} func (*ScopeResolution) typeNode() {} func (e *ScopeResolution) forEachChild(visit func(Node)) { - visit(e.Module) - visit(e.Name) + for _, segment := range e.Segments { + visit(segment.Name) + for _, arg := range segment.TypeArgs { + visit(arg) + } + } } func (e *ScopeResolution) loc() *source.Location { return e.Location } func (e *ScopeResolution) exprText() string { @@ -58,21 +67,32 @@ func (e *ScopeResolution) TypeText() string { if e == nil { return "" } - module := "" - if e.Module != nil { - module = e.Module.Name - } - name := "" - if e.Name != nil { - name = e.Name.Name + parts := make([]string, 0, len(e.Segments)) + for _, segment := range e.Segments { + parts = append(parts, appliedTypeText(segment.Name, segment.TypeArgs)) } - if module == "" { - return name + return strings.Join(parts, "::") +} + +// ImportMember accepts only paths whose first segment is an import qualifier +// and whose second segment is the imported symbol. Longer paths belong to +// enum/type member resolution in later semantic work. +func (e *ScopeResolution) ImportMember() (qualifier, member *Ident, ok bool) { + if e == nil || len(e.Segments) != 2 || len(e.Segments[0].TypeArgs) != 0 || + e.Segments[0].Name == nil || e.Segments[1].Name == nil { + return nil, nil, false } - if name == "" { - return module + "::" + return e.Segments[0].Name, e.Segments[1].Name, true +} + +// ImportValueMember excludes applied final segments because generic functions +// and values are not part of named-type application support. +func (e *ScopeResolution) ImportValueMember() (qualifier, member *Ident, ok bool) { + qualifier, member, ok = e.ImportMember() + if !ok || len(e.Segments[1].TypeArgs) != 0 { + return nil, nil, false } - return module + "::" + name + return qualifier, member, true } func (e *ScopeResolution) copyExpr(substitutions map[string]Expr, newID func(NodeID, bool) NodeID, fromArgument bool) Expr { @@ -80,7 +100,21 @@ func (e *ScopeResolution) copyExpr(substitutions map[string]Expr, newID func(Nod return nil } id := newID(e.ID(), fromArgument) - return &ScopeResolution{NodeIDHolder: NodeIDHolder{NodeID: id}, Module: cloneIdent(e.Module, newID, fromArgument), Name: cloneIdent(e.Name, newID, fromArgument), Location: e.Location} + return &ScopeResolution{NodeIDHolder: NodeIDHolder{NodeID: id}, Segments: clonePathSegments(e.Segments, newID, fromArgument), Location: e.Location} +} + +func appliedTypeText(name *Ident, args []TypeExpr) string { + if name == nil { + return "" + } + if len(args) == 0 { + return name.Name + } + parts := make([]string, len(args)) + for index, arg := range args { + parts[index] = TypeText(arg) + } + return name.Name + "<" + strings.Join(parts, ", ") + ">" } type SelectorExpr struct { diff --git a/internal/frontend/ast/node.go b/internal/frontend/ast/node.go index 3ea25340..6b5d76a4 100644 --- a/internal/frontend/ast/node.go +++ b/internal/frontend/ast/node.go @@ -50,6 +50,7 @@ type TypeDecl interface { Decl AttributedNode DeclName() *Ident + DeclarationTypeParams() []TypeParam UnderlyingType() TypeExpr } diff --git a/internal/frontend/parser/parse_expr.go b/internal/frontend/parser/parse_expr.go index efa95ec0..596100f9 100644 --- a/internal/frontend/parser/parse_expr.go +++ b/internal/frontend/parser/parse_expr.go @@ -492,20 +492,51 @@ func (p *Parser) parseIdentExpr() ast.Expr { if id == nil { return nil } - if p.match(token.DCOLON) { - member := p.parseIdent() - if member == nil { + first := ast.PathSegment{Name: id, Location: id.Location} + if p.at(token.LT) && p.typeArgumentsAreFollowedByScope() { + args, close, ok := p.parseTypeArguments() + if !ok { return nil } - return reg(p, &ast.ScopeResolution{ - Module: id, - Name: member, - Location: source.NewLocation(p.filePath, ast.StartOf(id), ast.EndOf(member)), - }) + first.TypeArgs = args + first.Location = source.NewLocation(p.filePath, ast.StartOf(id), close.End) + } + if p.at(token.DCOLON) { + path := p.parseScopeResolution(first) + if path == nil { + return nil + } + return path } return id } +func (p *Parser) typeArgumentsAreFollowedByScope() bool { + if !p.at(token.LT) { + return false + } + depth := 0 + for index := p.pos; index < len(p.stream); index++ { + switch p.stream[index].Kind { + case token.LT: + depth++ + case token.GT: + depth-- + case token.SHR: + depth -= 2 + case token.EOF, token.SEMICOLON: + return false + } + if depth < 0 { + return false + } + if depth == 0 { + return index+1 < len(p.stream) && p.stream[index+1].Kind == token.DCOLON + } + } + return false +} + func (p *Parser) parseSelector(left ast.Expr) ast.Expr { dot := p.consume(token.DOT, "expected '.'") if dot == nil { diff --git a/internal/frontend/parser/parse_types.go b/internal/frontend/parser/parse_types.go index 6463b6d2..14005a91 100644 --- a/internal/frontend/parser/parse_types.go +++ b/internal/frontend/parser/parse_types.go @@ -37,23 +37,21 @@ func (p *Parser) parseTypeExpr() ast.TypeExpr { case token.ENUM: return p.parseEnumTypeExpr() case token.IDENT: - p.advance() - id := reg(p, &ast.Ident{Name: tok.Literal, Location: source.NewLocation(p.filePath, tok.Start, tok.End)}) - if p.match(token.DCOLON) { - next := p.current() - if next.Kind != token.IDENT { - p.diag.Add(diagnostics.NewError("expected type segment after '::'").WithCode(diagnostics.ErrInvalidTypeInParser).WithPrimaryLabel(source.NewLocation(p.filePath, next.Start, next.End), fmt.Sprintf("found %s", next.Kind))) + first, ok := p.parsePathSegment() + if !ok { + return nil + } + if p.at(token.DCOLON) { + path := p.parseScopeResolution(first) + if path == nil { return nil } - p.advance() - member := reg(p, &ast.Ident{Name: next.Literal, Location: source.NewLocation(p.filePath, next.Start, next.End)}) - return reg(p, &ast.ScopeResolution{ - Module: id, - Name: member, - Location: source.NewLocation(p.filePath, tok.Start, next.End), - }) + return path } - return reg(p, &ast.NamedType{Name: id.Name, Location: id.Location}) + if len(first.TypeArgs) > 0 { + return reg(p, &ast.AppliedType{Name: first.Name, TypeArgs: first.TypeArgs, Location: first.Location}) + } + return reg(p, &ast.NamedType{Name: first.Name.Name, Location: first.Location}) default: loc := source.NewLocation(p.filePath, tok.Start, tok.End) d := diagnostics.NewError("expected type"). @@ -64,6 +62,73 @@ func (p *Parser) parseTypeExpr() ast.TypeExpr { } } +func (p *Parser) parsePathSegment() (ast.PathSegment, bool) { + name := p.parseIdent() + if name == nil { + return ast.PathSegment{}, false + } + segment := ast.PathSegment{Name: name, Location: name.Location} + if p.at(token.LT) { + args, close, ok := p.parseTypeArguments() + if !ok { + return ast.PathSegment{}, false + } + segment.TypeArgs = args + segment.Location = source.NewLocation(p.filePath, ast.StartOf(name), close.End) + } + return segment, true +} + +func (p *Parser) parseScopeResolution(first ast.PathSegment) *ast.ScopeResolution { + segments := []ast.PathSegment{first} + for p.match(token.DCOLON) { + segment, ok := p.parsePathSegment() + if !ok { + return nil + } + segments = append(segments, segment) + } + return reg(p, &ast.ScopeResolution{ + Segments: segments, + Location: source.NewLocation(p.filePath, ast.StartOf(first.Name), *segments[len(segments)-1].Location.End), + }) +} + +func (p *Parser) parseTypeArguments() ([]ast.TypeExpr, *token.Token, bool) { + open := p.consume(token.LT, "expected '<' before type arguments") + if open == nil { + return nil, nil, false + } + args := make([]ast.TypeExpr, 0, 1) + for { + arg := p.parseTypeExpr() + if arg == nil { + return nil, nil, false + } + args = append(args, arg) + if !p.match(token.COMMA) { + break + } + } + close := p.consumeTypeArgumentClose(open.Start) + return args, close, close != nil +} + +func (p *Parser) consumeTypeArgumentClose(open source.Position) *token.Token { + if p.at(token.GT) { + return p.advance() + } + if p.at(token.SHR) { + combined := p.current() + middle := combined.Start + middle.Advance(">") + first := token.Token{Kind: token.GT, Literal: ">", Start: combined.Start, End: middle} + p.stream[p.pos] = token.Token{Kind: token.GT, Literal: ">", Start: middle, End: combined.End} + return &first + } + return p.expectClose(open, token.GT, "<") +} + func (p *Parser) parseRefTypeExpr() ast.TypeExpr { start := p.consume(token.AMP, "expected '&' in reference type") if start == nil { diff --git a/internal/frontend/parser/parser_test.go b/internal/frontend/parser/parser_test.go index bb600013..58ca7d26 100644 --- a/internal/frontend/parser/parser_test.go +++ b/internal/frontend/parser/parser_test.go @@ -211,6 +211,61 @@ func TestParseFunctionWithTypeParams(t *testing.T) { } } +func TestParseAppliedNamedTypesAndNestedClosers(t *testing.T) { + mod, diag := parseTestModule(`fn transform(value: Box) -> pkg::Outer> { + return value; +}`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + fn := mod.Stmts[0].(*ast.FnDecl) + param, ok := fn.Params[0].Type.(*ast.AppliedType) + if !ok || ast.TypeText(param) != "Box" { + t.Fatalf("parameter type = %#v, want Box", fn.Params[0].Type) + } + qualified, ok := fn.ReturnType.(*ast.ScopeResolution) + if !ok || len(qualified.Segments) != 2 || ast.TypeText(qualified) != "pkg::Outer>" { + t.Fatalf("return type = %#v, want pkg::Outer>", fn.ReturnType) + } + if len(qualified.Segments[1].TypeArgs) != 1 { + t.Fatalf("return type arguments = %#v", qualified.Segments[1].TypeArgs) + } +} + +func TestParseGenericQualifiedExpressionPath(t *testing.T) { + mod, diag := parseTestModule(`fn main() { + pkg::Result::Ok; +}`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + stmt := mod.Stmts[0].(*ast.FnDecl).Body.Stmts[0].(*ast.ExprStmt) + path, ok := stmt.Expr.(*ast.ScopeResolution) + if !ok || len(path.Segments) != 3 || ast.ExprText(path) != "pkg::Result::Ok" { + t.Fatalf("expression = %#v, want three-segment generic path", stmt.Expr) + } + if len(path.Segments[0].TypeArgs) != 0 || len(path.Segments[1].TypeArgs) != 1 || len(path.Segments[2].TypeArgs) != 0 { + t.Fatalf("path type arguments = %#v", path.Segments) + } +} + +func TestParseComparisonAndShiftRemainExpressions(t *testing.T) { + mod, diag := parseTestModule(`fn compare(left: i32, right: i32) -> bool { + let less = left < right; + let shifted = left >> right; + return less; +}`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + body := mod.Stmts[0].(*ast.FnDecl).Body + less := body.Stmts[0].(*ast.LetDecl).Value.(*ast.BinaryExpr) + shifted := body.Stmts[1].(*ast.LetDecl).Value.(*ast.BinaryExpr) + if less.Op != "<" || shifted.Op != ">>" { + t.Fatalf("operators = %q and %q, want < and >>", less.Op, shifted.Op) + } +} + func TestParseValueParam(t *testing.T) { src := `fn destroy(data: Buffer) {}` mod, diag := parseTestModule(src) diff --git a/internal/ir/hir/lower/module_lower.go b/internal/ir/hir/lower/module_lower.go index 23a45344..ac7df2ad 100644 --- a/internal/ir/hir/lower/module_lower.go +++ b/internal/ir/hir/lower/module_lower.go @@ -522,8 +522,10 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s sym = module.Semantics.ResolvedSymbols[node.ID()] } if sym == nil { - if resolved, ok := project.LookupImportedSymbol(ctx, module, node.Module.Name, node.Name.Name); ok { - sym = resolved.Symbol + if qualifier, member, imported := node.ImportValueMember(); imported { + if resolved, ok := project.LookupImportedSymbol(ctx, module, qualifier.Name, member.Name); ok { + sym = resolved.Symbol + } } } if sym != nil { @@ -537,7 +539,7 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s } return &ir.Ident{Name: symbolName(module, sym), Type: t, SymbolID: sym.ID, SourceInfo: ir.SourceInfo{Location: loc}} } - return &ir.InvalidExpr{Message: "unresolved qualified identifier: " + node.Module.Name + "::" + node.Name.Name, Type: ir.InvalidType, SourceInfo: ir.SourceInfo{Location: loc}} + return &ir.InvalidExpr{Message: "unresolved qualified identifier: " + node.TypeText(), Type: ir.InvalidType, SourceInfo: ir.SourceInfo{Location: loc}} case *ast.UnaryExpr: arg := lowerASTExpr(ctx, module, scope, node.Expr, expectedType) @@ -642,8 +644,10 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s sym = s } case *ast.ScopeResolution: - if resolved, ok := project.LookupImportedSymbol(ctx, module, callee.Module.Name, callee.Name.Name); ok && resolved.Symbol != nil { - sym = resolved.Symbol + if qualifier, member, imported := callee.ImportValueMember(); imported { + if resolved, ok := project.LookupImportedSymbol(ctx, module, qualifier.Name, member.Name); ok && resolved.Symbol != nil { + sym = resolved.Symbol + } } } if sym != nil { diff --git a/internal/lsp/cursor.go b/internal/lsp/cursor.go index ae950d65..bfcd46e8 100644 --- a/internal/lsp/cursor.go +++ b/internal/lsp/cursor.go @@ -113,20 +113,23 @@ func resolveIdentSymbol(ident *ast.Ident, parents map[ast.NodeID]ast.Node, modul } // 2. Check if it's a scope resolution member (M::x) - if sr, ok := parent.(*ast.ScopeResolution); ok && sr.Name == ident { - qualifier := sr.Module.Name - if imp, ok := module.Imports[qualifier]; ok { - if mod, ok := ctx.ModuleByKey(imp.Key); ok && mod.ModuleScope != nil { - if sym, ok := mod.ModuleScope.LookupLocal(ident.Name); ok { - return sym + if sr, ok := parent.(*ast.ScopeResolution); ok { + qualifierNode, memberNode, imported := sr.ImportMember() + if imported && memberNode == ident { + qualifier := qualifierNode.Name + if imp, ok := module.Imports[qualifier]; ok { + if mod, ok := ctx.ModuleByKey(imp.Key); ok && mod.ModuleScope != nil { + if sym, ok := mod.ModuleScope.LookupLocal(ident.Name); ok { + return sym + } } } + return nil } - return nil } // 3. Check if it's a scope resolution qualifier (M::x) - if sr, ok := parent.(*ast.ScopeResolution); ok && sr.Module == ident { + if sr, ok := parent.(*ast.ScopeResolution); ok && len(sr.Segments) > 1 && sr.Segments[0].Name == ident { qualifier := ident.Name if imp, ok := module.Imports[qualifier]; ok { sym := symbols.New(ident.Name, symbols.SymbolImport, parent, ast.LocOf(ident)) diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index 32bf5bef..5e97a033 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -98,7 +98,7 @@ func resolveImportHoverSubject(cc *cursorContext) *hoverSubject { return nil } parent := cc.parents[ident.ID()] - if sr, ok := parent.(*ast.ScopeResolution); ok && sr.Module == ident { + if sr, ok := parent.(*ast.ScopeResolution); ok && len(sr.Segments) > 1 && sr.Segments[0].Name == ident { imp, ok := cc.module.Imports[ident.Name] if !ok { return nil @@ -160,7 +160,7 @@ func hoverTypeNode(node ast.Node, parents map[ast.NodeID]ast.Node) (ast.TypeExpr top := node for top != nil { switch top.(type) { - case *ast.Ident, *ast.ScopeResolution: + case *ast.Ident, *ast.AppliedType, *ast.ScopeResolution: parent := parents[top.ID()] if parent == nil { top = nil @@ -170,6 +170,10 @@ func hoverTypeNode(node ast.Node, parents map[ast.NodeID]ast.Node) (ast.TypeExpr top = parent continue } + if _, ok := parent.(*ast.AppliedType); ok { + top = parent + continue + } typeNode, ok := top.(ast.TypeExpr) if ok && isTypeExprPosition(typeNode, parent) { return typeNode, true @@ -213,6 +217,20 @@ func isTypeExprPosition(typeNode ast.TypeExpr, parent ast.Node) bool { return p.TypeExpr == typeNode case *ast.StructLit: return p.Type == typeNode + case *ast.AppliedType: + for _, arg := range p.TypeArgs { + if arg == typeNode { + return true + } + } + case *ast.ScopeResolution: + for _, segment := range p.Segments { + for _, arg := range segment.TypeArgs { + if arg == typeNode { + return true + } + } + } case *ast.OwnedPtrType: return p.Target == typeNode case *ast.RefType: @@ -671,7 +689,7 @@ func hoverTypeLabel(typ typeinfo.Type) (string, bool) { if t == nil { return "", false } - return t.Name, true + return t.Text(), true case *typeinfo.StructType, *typeinfo.InterfaceType, *typeinfo.EnumType: return "", false default: diff --git a/internal/lsp/server_test.go b/internal/lsp/server_test.go index a68cff88..71f2e9e5 100644 --- a/internal/lsp/server_test.go +++ b/internal/lsp/server_test.go @@ -930,6 +930,22 @@ func TestHoverShowsInlineTypeSyntax(t *testing.T) { } } +func TestHoverShowsAppliedNamedType(t *testing.T) { + root := t.TempDir() + mainPath := filepath.Join(root, "main"+peeper.SourceExt) + src := "struct Box { value: T }\nfn use(value: __CURSOR__Box) {}\n" + + state := NewServerState() + state.RootDir = root + hover := hoverAtSource(t, state, mainPath, src) + if hover == nil { + t.Fatal("expected applied type hover") + } + if !strings.Contains(hover.Contents.Value, "(type) Box") { + t.Fatalf("unexpected applied type hover: %q", hover.Contents.Value) + } +} + func TestHoverShowsSelectorMemberFieldType(t *testing.T) { root := t.TempDir() mainPath := filepath.Join(root, "main"+peeper.SourceExt) diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index da6db66b..bc4119d2 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -1413,6 +1413,86 @@ fn main() -> i32 { } } +func TestPipelineLowersConcreteGenericStructInstance(t *testing.T) { + entrySrc := `struct Box { value: T } + +fn Read(box: &Box) -> i32 { return box.value; } +fn main() -> i32 { + let box: Box = .{ value = 42 }; + return Read(&box); +}` + const entryPath = "entry" + peeper.SourceExt + diag := diagnostics.NewDiagnosticBag() + diag.AddSourceContent(entryPath, entrySrc) + ctx := project.NewWithConfig(project.Config{RootDir: ".", Extension: peeper.SourceExt}, diag) + entry := parseModuleSource(entryPath, entrySrc, diag) + entry.Origin = project.ModuleOriginLocal + + if err := Run(ctx, entry); err != nil { + t.Fatalf("pipeline.Run returned error: %v", err) + } + if diag.HasErrors() { + t.Fatalf("unexpected generic pipeline diagnostics:\n%s", diag.EmitAllToString()) + } + if entry.HIR == nil || entry.MIR == nil || entry.LLVMIR == "" { + t.Fatal("generic named type did not reach HIR, MIR, and LLVM") + } +} + +func TestPipelineResolvesImportedGenericApplication(t *testing.T) { + diag := runImportedRuntimeSymbolPipeline(t, `import "app/runtime"; + +fn Read(box: &runtime::Box) -> i32 { return box.value; } +fn main() -> i32 { + let box: runtime::Box = .{ value = 9 }; + return Read(&box); +}`, `struct Box { value: T }`) + if diag.HasErrors() { + t.Fatalf("unexpected imported generic diagnostics:\n%s", diag.EmitAllToString()) + } +} + +func TestPipelineRejectsTypeArgumentsOnImportedValuePath(t *testing.T) { + diag := runImportedRuntimeSymbolPipeline(t, `import "app/runtime"; + +fn main() -> i32 { + return runtime::Make(); +}`, `fn Make() -> i32 { return 42; }`) + out := diag.EmitAllToString() + if !diag.HasErrors() || !strings.Contains(out, diagnostics.ErrInvalidType) || + !strings.Contains(out, "type arguments are not allowed on value paths") { + t.Fatalf("expected rejected imported value type arguments, got:\n%s", out) + } +} + +func TestPipelineLowersGenericInterfaceInstance(t *testing.T) { + entrySrc := `iface Reader { fn (&Self) read() -> T } +struct Counter { value: i32 } + +fn (self: &Counter) read() -> i32 { return self.value; } +fn Read(reader: &Reader) -> i32 { return reader.read(); } +fn main() -> i32 { + let counter: Counter = .{ value = 17 }; + return Read(&counter); +}` + const entryPath = "entry" + peeper.SourceExt + diag := diagnostics.NewDiagnosticBag() + diag.AddSourceContent(entryPath, entrySrc) + ctx := project.NewWithConfig(project.Config{RootDir: ".", Extension: peeper.SourceExt}, diag) + entry := parseModuleSource(entryPath, entrySrc, diag) + entry.Origin = project.ModuleOriginLocal + + if err := Run(ctx, entry); err != nil { + t.Fatalf("pipeline.Run returned error: %v", err) + } + if diag.HasErrors() { + t.Fatalf("unexpected generic interface diagnostics:\n%s", diag.EmitAllToString()) + } + if entry.MIR == nil || entry.LLVMIR == "" { + t.Fatal("generic interface instance did not reach MIR and LLVM") + } +} + func TestPipelineLowersArrayIndexRead(t *testing.T) { preludeSrc := `` entrySrc := `fn first(xs: [4]i32) -> i32 { diff --git a/internal/project/context.go b/internal/project/context.go index 2d13b104..aac55a57 100644 --- a/internal/project/context.go +++ b/internal/project/context.go @@ -47,6 +47,10 @@ type CompilerContext struct { fileIndex map[string]string // Prior semantic API fingerprints supplied by incremental clients. semanticExportBaselines map[string]string + // Named declaration identity -> declaration syntax and owning module. + typeDeclarations map[string]namedTypeDeclaration + // Concrete semantic application identity -> canonical instance. + typeInstances map[string]namedTypeInstance // Shared compiler dependency graph. Graph *graph.Graph @@ -169,6 +173,8 @@ func NewWithConfig(cfg Config, diag *diagnostics.DiagnosticBag) *CompilerContext modules: make(map[string]*Module), fileIndex: make(map[string]string), semanticExportBaselines: make(map[string]string), + typeDeclarations: make(map[string]namedTypeDeclaration), + typeInstances: make(map[string]namedTypeInstance), } } @@ -188,6 +194,20 @@ func (ctx *CompilerContext) ResetModule(module *Module, retained phase.Phase) { return } module.resetToPhase(retained) + ctx.mu.Lock() + for identity, instance := range ctx.typeInstances { + if instance.ownerModuleKey == module.Key { + delete(ctx.typeInstances, identity) + } + } + if retained < phase.Collected { + for identity, declaration := range ctx.typeDeclarations { + if declaration.module == module { + delete(ctx.typeDeclarations, identity) + } + } + } + ctx.mu.Unlock() if ctx.Diagnostics != nil && module.Key != "" { ctx.Diagnostics.DiscardModuleAfter(module.Key, retained) } diff --git a/internal/project/export_fingerprint.go b/internal/project/export_fingerprint.go index 64a05722..4bee767e 100644 --- a/internal/project/export_fingerprint.go +++ b/internal/project/export_fingerprint.go @@ -108,7 +108,18 @@ func semanticTypeKey(typ symbols.Type, visiting map[typeinfo.Type]bool) string { switch node := semantic.(type) { case *typeinfo.DefinedType: - return "defined(" + node.Name + ":" + semanticTypeKey(node.Underlying, visiting) + ")" + parameters := make([]string, len(node.TypeParameters)) + for index, parameter := range node.TypeParameters { + parameters[index] = semanticTypeKey(parameter, visiting) + } + arguments := make([]string, len(node.TypeArguments)) + for index, argument := range node.TypeArguments { + arguments[index] = semanticTypeKey(argument, visiting) + } + return fmt.Sprintf("defined(%d:%s<%s>[%s]:%s)", node.Kind, node.Name, + strings.Join(parameters, ","), strings.Join(arguments, ","), semanticTypeKey(node.Underlying, visiting)) + case *typeinfo.TypeParameterType: + return fmt.Sprintf("parameter(%s:%d:%s)", node.OwnerIdentity, node.Index, node.Name) case *typeinfo.OwnedPtrType: return "owned(" + semanticTypeKey(node.Target, visiting) + ")" case *typeinfo.RefType: diff --git a/internal/project/generic_types.go b/internal/project/generic_types.go new file mode 100644 index 00000000..32d93362 --- /dev/null +++ b/internal/project/generic_types.go @@ -0,0 +1,99 @@ +package project + +import ( + "strconv" + "strings" + + "compiler/internal/diagnostics" + "compiler/internal/frontend/ast" + "compiler/internal/semantics/typeinfo" +) + +type namedTypeDeclaration struct { + module *Module + syntax ast.TypeDecl + base *typeinfo.DefinedType +} + +type namedTypeInstance struct { + ownerModuleKey string + typ *typeinfo.DefinedType +} + +// RegisterTypeDeclaration preserves declaration syntax beside its stable +// semantic shell so concrete applications can substitute from one source. +func (ctx *CompilerContext) RegisterTypeDeclaration(module *Module, declaration ast.TypeDecl, base *typeinfo.DefinedType) { + if ctx == nil || module == nil || declaration == nil || base == nil || base.Identity == "" { + return + } + ctx.mu.Lock() + ctx.typeDeclarations[base.Identity] = namedTypeDeclaration{module: module, syntax: declaration, base: base} + ctx.mu.Unlock() +} + +func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, arguments []typeinfo.Type, node ast.TypeExpr) typeinfo.Type { + if ctx == nil || base == nil || len(arguments) != len(base.TypeParameters) { + return &typeinfo.InvalidType{} + } + declarationArguments := true + for index, argument := range arguments { + if argument != base.TypeParameters[index] { + declarationArguments = false + break + } + } + if declarationArguments { + return base + } + + argumentKeys := make([]string, len(arguments)) + for index, argument := range arguments { + argumentKeys[index] = typeArgumentIdentity(argument) + } + identity := base.Identity + "<" + strings.Join(argumentKeys, ",") + ">" + + ctx.mu.Lock() + if cached, ok := ctx.typeInstances[identity]; ok && cached.typ != nil { + ctx.mu.Unlock() + return cached.typ + } + declaration, ok := ctx.typeDeclarations[base.Identity] + if !ok || declaration.module == nil || declaration.syntax == nil || declaration.base != base { + ctx.mu.Unlock() + if ctx.Diagnostics != nil { + ctx.Diagnostics.AddError(diagnostics.ErrInvalidType, + "generic type declaration is unavailable for `"+base.Text()+"`", ast.LocOf(node), "recompile declaration module") + } + return &typeinfo.InvalidType{} + } + instance := &typeinfo.DefinedType{ + Name: base.Name, + Identity: identity, + Kind: base.Kind, + TypeParameters: base.TypeParameters, + TypeArguments: append([]typeinfo.Type(nil), arguments...), + } + // Cache provisional shell before substitution. Recursive pointer/reference + // applications resolve back to this exact object. + ctx.typeInstances[identity] = namedTypeInstance{ownerModuleKey: declaration.module.Key, typ: instance} + ctx.mu.Unlock() + + opts := TypeSyntaxOptions(ctx, declaration.module, nil, true) + opts.TypeParameters = typeinfo.TypeParameterBindings(base.TypeParameters, arguments) + instance.Underlying = typeinfo.TypeFromSyntax(declaration.syntax.UnderlyingType(), opts) + return instance +} + +func typeArgumentIdentity(typ typeinfo.Type) string { + switch value := typ.(type) { + case *typeinfo.DefinedType: + if value != nil && value.Identity != "" { + return "defined:" + value.Identity + } + case *typeinfo.TypeParameterType: + if value != nil { + return "parameter:" + value.OwnerIdentity + ":" + strconv.Itoa(value.Index) + ":" + value.Text() + } + } + return semanticTypeKey(typ, make(map[typeinfo.Type]bool)) +} diff --git a/internal/project/modules_test.go b/internal/project/modules_test.go index 1d0e8729..f66a69c5 100644 --- a/internal/project/modules_test.go +++ b/internal/project/modules_test.go @@ -104,3 +104,25 @@ func TestCompilerContextResetModuleDiscardsOnlyDownstreamDiagnostics(t *testing. t.Fatalf("module artifacts after reset = %#v", module) } } + +func TestCompilerContextResetPurgesOwnedNamedTypeInstances(t *testing.T) { + ctx := New(".", ".peep", nil) + module := &Module{Key: "owner"} + ctx.typeInstances["owner::Box"] = namedTypeInstance{ + ownerModuleKey: module.Key, + typ: &typeinfo.DefinedType{Name: "Box", Identity: "owner::Box"}, + } + ctx.typeInstances["other::Box"] = namedTypeInstance{ + ownerModuleKey: "other", + typ: &typeinfo.DefinedType{Name: "Box", Identity: "other::Box"}, + } + + ctx.ResetModule(module, phase.Parsed) + + if _, found := ctx.typeInstances["owner::Box"]; found { + t.Fatal("reset retained instance owned by reset module") + } + if _, found := ctx.typeInstances["other::Box"]; !found { + t.Fatal("reset removed instance owned by another module") + } +} diff --git a/internal/project/type_syntax.go b/internal/project/type_syntax.go index 30b6b172..834883d3 100644 --- a/internal/project/type_syntax.go +++ b/internal/project/type_syntax.go @@ -41,6 +41,9 @@ func TypeSyntaxOptions(ctx *CompilerContext, module *Module, selfType typeinfo.T } return symbols.GetSymbolType(resolved.Symbol) }, + Instantiate: func(base *typeinfo.DefinedType, arguments []typeinfo.Type, node ast.TypeExpr) typeinfo.Type { + return ctx.instantiateType(base, arguments, node) + }, InvalidSelf: func(node *ast.NamedType) typeinfo.Type { if ctx != nil && ctx.Diagnostics != nil { ctx.Diagnostics.AddError(diagnostics.ErrInvalidType, @@ -56,5 +59,17 @@ func TypeSyntaxOptions(ctx *CompilerContext, module *Module, selfType typeinfo.T } return &typeinfo.InvalidType{} }, + InvalidApplication: func(node ast.TypeExpr, name string, want, got int) typeinfo.Type { + if ctx != nil && ctx.Diagnostics != nil { + word := "arguments" + if want == 1 { + word = "argument" + } + ctx.Diagnostics.AddError(diagnostics.ErrInvalidType, + fmt.Sprintf("type `%s` expects %d type %s, got %d", name, want, word, got), + ast.LocOf(node), "use exact explicit type arguments") + } + return &typeinfo.InvalidType{} + }, } } diff --git a/internal/semantics/binder/binder.go b/internal/semantics/binder/binder.go index 8efa69d4..8cb40651 100644 --- a/internal/semantics/binder/binder.go +++ b/internal/semantics/binder/binder.go @@ -97,19 +97,21 @@ func (b *binder) bindTypeDecl(decl ast.TypeDecl) { if sym == nil { return } - underlying := typeinfo.TypeFromSyntax(typ, project.TypeSyntaxOptions(b.ctx, b.module, nil, true)) - if defined, ok := sym.Type.(*typeinfo.DefinedType); ok && defined != nil { + defined, ok := sym.Type.(*typeinfo.DefinedType) + if ok && defined != nil { // Reuse same shell so self-references keep same type identity. defined.Name = name.Name defined.Identity = b.module.TypeDeclarationIdentity(name.Name) - defined.Underlying = underlying } else { - sym.BindType(&typeinfo.DefinedType{ - Name: name.Name, - Identity: b.module.TypeDeclarationIdentity(name.Name), - Underlying: underlying, - }) + defined = &typeinfo.DefinedType{ + Name: name.Name, + Identity: b.module.TypeDeclarationIdentity(name.Name), + } + sym.BindType(defined) } + opts := project.TypeSyntaxOptions(b.ctx, b.module, nil, true) + opts.TypeParameters = typeinfo.TypeParameterBindings(defined.TypeParameters, nil) + defined.Underlying = typeinfo.TypeFromSyntax(typ, opts) b.registerTypeDecl(name.Name, typ) } diff --git a/internal/semantics/binder/binder_test.go b/internal/semantics/binder/binder_test.go index be1adee3..333df9e8 100644 --- a/internal/semantics/binder/binder_test.go +++ b/internal/semantics/binder/binder_test.go @@ -1,6 +1,7 @@ package binder import ( + "strings" "testing" "compiler/internal/diagnostics" @@ -8,6 +9,8 @@ import ( "compiler/internal/frontend/parser" "compiler/internal/project" "compiler/internal/semantics/collector" + "compiler/internal/semantics/symbols" + "compiler/internal/semantics/typeinfo" "compiler/pkg/peeper" ) @@ -96,3 +99,110 @@ struct B { a: A }`, }) } } + +func TestBindInstantiatesGenericNamedTypes(t *testing.T) { + const filePath = "binder_generic_instances_test" + peeper.SourceExt + const src = `struct Box { value: T } +struct Node { next: *Node } +type Maybe = ?T; +iface Reader { fn (&Self) read() -> T } +enum Choice { Left, Right } +fn Use(box: Box, again: Box, other: Box, nested: Box>, node: Node, maybe: Maybe, reader: Reader, choice: Choice) {}` + diag := diagnostics.NewDiagnosticBag() + ctx := project.New(".", peeper.SourceExt, diag) + module := &project.Module{ + Key: project.ModuleKeyFor(project.ModuleOriginLocal, filePath), + FilePath: filePath, + Content: src, + AST: parser.New(filePath, lexer.New(filePath, src, diag).Tokenize(), diag).ParseModule(), + Imports: make(map[string]project.ResolvedImport), + } + collector.Collect(ctx, module) + Bind(ctx, module) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + + use, ok := module.ModuleScope.LookupLocal("Use") + if !ok || use == nil || use.Kind != symbols.SymbolFunc { + t.Fatal("missing Use function") + } + fn, ok := use.Type.(*typeinfo.FuncType) + if !ok || len(fn.Params) != 8 { + t.Fatalf("Use type = %#v", use.Type) + } + box, ok := fn.Params[0].(*typeinfo.DefinedType) + if !ok || box.Text() != "Box" { + t.Fatalf("Box instance = %#v", fn.Params[0]) + } + if fn.Params[1] != box { + t.Fatal("repeated Box applications must reuse one cached instance") + } + other, ok := fn.Params[2].(*typeinfo.DefinedType) + if !ok || other == box || other.Identity == box.Identity { + t.Fatalf("Box instance = %#v, want distinct semantic identity", fn.Params[2]) + } + nested, ok := fn.Params[3].(*typeinfo.DefinedType) + if !ok { + t.Fatalf("nested Box instance = %#v", fn.Params[3]) + } + nestedStruct, ok := typeinfo.Underlying(nested).(*typeinfo.StructType) + if !ok || len(nestedStruct.Fields) != 1 || nestedStruct.Fields[0].Type != box { + t.Fatalf("nested Box payload = %#v, want cached Box", nestedStruct) + } + node, ok := fn.Params[4].(*typeinfo.DefinedType) + if !ok { + t.Fatalf("Node instance = %#v", fn.Params[4]) + } + nodeStruct, ok := typeinfo.Underlying(node).(*typeinfo.StructType) + if !ok || len(nodeStruct.Fields) != 1 { + t.Fatalf("Node payload = %#v", nodeStruct) + } + next, ok := nodeStruct.Fields[0].Type.(*typeinfo.OwnedPtrType) + if !ok || next.Target != node { + t.Fatalf("recursive Node target = %#v, want provisional instance", nodeStruct.Fields[0].Type) + } + maybe, ok := typeinfo.Underlying(fn.Params[5]).(*typeinfo.OptionalType) + if !ok || !typeinfo.SameType(maybe.Inner, &typeinfo.IntegerType{Signed: true, Bits: 32}) { + t.Fatalf("Maybe payload = %#v", fn.Params[5]) + } + reader, ok := typeinfo.Underlying(fn.Params[6]).(*typeinfo.InterfaceType) + if !ok || len(reader.Methods) != 1 || !typeinfo.SameType(reader.Methods[0].Return, &typeinfo.IntegerType{Signed: true, Bits: 32}) { + t.Fatalf("Reader payload = %#v", fn.Params[6]) + } + choice, ok := fn.Params[7].(*typeinfo.DefinedType) + if !ok || choice.Text() != "Choice" { + t.Fatalf("Choice instance = %#v", fn.Params[7]) + } +} + +func TestBindRequiresExactNamedTypeArguments(t *testing.T) { + tests := []struct { + name string + source string + want string + }{ + {name: "missing", source: `struct Box { value: T } fn Use(value: Box) {}`, want: "expects 1 type argument"}, + {name: "extra", source: `struct Box { value: T } fn Use(value: Box) {}`, want: "expects 1 type argument"}, + {name: "nongeneric", source: `struct Plain {} fn Use(value: Plain) {}`, want: "expects 0 type arguments"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + const filePath = "binder_generic_arity_test" + peeper.SourceExt + diag := diagnostics.NewDiagnosticBag() + ctx := project.New(".", peeper.SourceExt, diag) + module := &project.Module{ + Key: project.ModuleKeyFor(project.ModuleOriginLocal, filePath), + FilePath: filePath, + Content: test.source, + AST: parser.New(filePath, lexer.New(filePath, test.source, diag).Tokenize(), diag).ParseModule(), + Imports: make(map[string]project.ResolvedImport), + } + collector.Collect(ctx, module) + Bind(ctx, module) + if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), test.want) { + t.Fatalf("expected %q diagnostic, got:\n%s", test.want, diag.EmitAllToString()) + } + }) + } +} diff --git a/internal/semantics/binder/type_decl_cycles.go b/internal/semantics/binder/type_decl_cycles.go index 3f149154..539cf515 100644 --- a/internal/semantics/binder/type_decl_cycles.go +++ b/internal/semantics/binder/type_decl_cycles.go @@ -83,6 +83,10 @@ func (b *binder) addTypeDeclEdges(owner graph.NodeID, typ ast.TypeExpr, indirect switch node := typ.(type) { case *ast.NamedType: b.addTypeDeclEdge(owner, b.lookupTypeDeclNodeID(node.Name), indirect) + case *ast.AppliedType: + if node.Name != nil { + b.addTypeDeclEdge(owner, b.lookupTypeDeclNodeID(node.Name.Name), indirect) + } case *ast.ScopeResolution: b.addTypeDeclEdge(owner, b.lookupQualifiedTypeDeclNodeID(node), indirect) case *ast.RawPtrType, *ast.EnumType: @@ -141,10 +145,14 @@ func (b *binder) lookupTypeDeclNodeID(name string) graph.NodeID { } func (b *binder) lookupQualifiedTypeDeclNodeID(node *ast.ScopeResolution) graph.NodeID { - if b == nil || b.ctx == nil || b.module == nil || node == nil || node.Module == nil || node.Name == nil { + if b == nil || b.ctx == nil || b.module == nil || node == nil { + return "" + } + qualifier, member, imported := node.ImportMember() + if !imported { return "" } - resolved, ok := project.LookupImportedSymbol(b.ctx, b.module, node.Module.Name, node.Name.Name) + resolved, ok := project.LookupImportedSymbol(b.ctx, b.module, qualifier.Name, member.Name) if !ok || resolved.Module == nil || resolved.Symbol == nil || resolved.Symbol.Kind != symbols.SymbolType { return "" } diff --git a/internal/semantics/collector/collector.go b/internal/semantics/collector/collector.go index 04bf57f7..adb0b7a3 100644 --- a/internal/semantics/collector/collector.go +++ b/internal/semantics/collector/collector.go @@ -42,7 +42,7 @@ func (c *collector) collectModule(mod *ast.Module) { func (c *collector) collectNode(node ast.Node) { if decl, ok := node.(ast.TypeDecl); ok { if name := decl.DeclName(); name != nil { - c.collectConcreteTypeDecl(name, node) + c.collectConcreteTypeDecl(decl) return } } @@ -101,24 +101,49 @@ func (c *collector) collectFnDecl(fn *ast.FnDecl) { } } -func (c *collector) collectConcreteTypeDecl(name *ast.Ident, node ast.Node) { - if c == nil || c.module == nil || node == nil { +func (c *collector) collectConcreteTypeDecl(decl ast.TypeDecl) { + if c == nil || c.module == nil || decl == nil { return } + name := decl.DeclName() if name == nil || name.Name == "" { - c.ctx.Diagnostics.AddError(diagnostics.ErrMissingIdentifier, "type name required", ast.LocOf(node), "") + c.ctx.Diagnostics.AddError(diagnostics.ErrMissingIdentifier, "type name required", ast.LocOf(decl), "") return } - sym := symbols.New(name.Name, symbols.SymbolType, node, ast.LocOf(name)) - sym.Type = &typeinfo.DefinedType{ - Name: name.Name, - Identity: c.module.TypeDeclarationIdentity(name.Name), + identity := c.module.TypeDeclarationIdentity(name.Name) + parameters := make([]*typeinfo.TypeParameterType, 0, len(decl.DeclarationTypeParams())) + for index, parameter := range decl.DeclarationTypeParams() { + if parameter.Name != nil && parameter.Name.Name != "" { + parameters = append(parameters, &typeinfo.TypeParameterType{ + Name: parameter.Name.Name, OwnerIdentity: identity, Index: index, + }) + } + } + kind := typeinfo.DefinedKindInvalid + switch decl.(type) { + case *ast.TypeAliasDecl: + kind = typeinfo.DefinedKindAlias + case *ast.StructDecl: + kind = typeinfo.DefinedKindStruct + case *ast.InterfaceDecl: + kind = typeinfo.DefinedKindInterface + case *ast.EnumDecl: + kind = typeinfo.DefinedKindEnum + } + sym := symbols.New(name.Name, symbols.SymbolType, decl, ast.LocOf(name)) + defined := &typeinfo.DefinedType{ + Name: name.Name, + Identity: identity, + Kind: kind, + TypeParameters: parameters, // Underlying is filled by binder. } + sym.Type = defined if err := c.module.ModuleScope.Declare(sym); err != nil { problems.ReportRedeclaration(c.ctx.Diagnostics, c.module.ModuleScope, err.Error(), name.Name, name.Location) return } + c.ctx.RegisterTypeDeclaration(c.module, decl, defined) } func (c *collector) collectModuleBinding(name *ast.Ident, kind symbols.Kind, node ast.Node) { diff --git a/internal/semantics/collector/collector_test.go b/internal/semantics/collector/collector_test.go index 139cf7bb..504074be 100644 --- a/internal/semantics/collector/collector_test.go +++ b/internal/semantics/collector/collector_test.go @@ -14,7 +14,7 @@ import ( "compiler/pkg/peeper" ) -var _ func(*collector, *ast.Ident, ast.Node) = (*collector).collectConcreteTypeDecl +var _ func(*collector, ast.TypeDecl) = (*collector).collectConcreteTypeDecl var _ func(*collector, *ast.Ident, symbols.Kind, ast.Node) = (*collector).collectModuleBinding func TestCallableSymbolsKeepDefiningModuleKey(t *testing.T) { diff --git a/internal/semantics/resolver/resolver.go b/internal/semantics/resolver/resolver.go index fffb0f8d..61ff24c8 100644 --- a/internal/semantics/resolver/resolver.go +++ b/internal/semantics/resolver/resolver.go @@ -238,7 +238,7 @@ func (r *resolver) resolveExpr(scope *symbols.Scope, expr ast.Expr) { } reportUnresolved(r.module, scope, node, r.ctx.Diagnostics) case *ast.ScopeResolution: - if r.resolveScopeResolution(node) { + if r.resolveScopeResolution(node, false) { return } case *ast.SelectorExpr: @@ -251,14 +251,14 @@ func (r *resolver) resolveExpr(scope *symbols.Scope, expr ast.Expr) { r.resolveExpr(scope, node.End) case *ast.StructLit: if scopedType, ok := node.Type.(*ast.ScopeResolution); ok { - r.resolveScopeResolution(scopedType) + r.resolveScopeResolution(scopedType, true) } for _, field := range node.Fields { r.resolveExpr(scope, field.Value) } case *ast.ArrayLit: if scopedType, ok := node.Type.(*ast.ScopeResolution); ok { - r.resolveScopeResolution(scopedType) + r.resolveScopeResolution(scopedType, true) } for _, value := range node.Values { r.resolveExpr(scope, value) @@ -312,12 +312,21 @@ func (r *resolver) resolveAssignTarget(scope *symbols.Scope, expr ast.Expr) { } } -func (r *resolver) resolveScopeResolution(node *ast.ScopeResolution) bool { +func (r *resolver) resolveScopeResolution(node *ast.ScopeResolution, allowTypeArguments bool) bool { if r == nil || r.module == nil || node == nil { return false } - qualifier := node.Module.Name - member := node.Name.Name + qualifierNode, memberNode, imported := node.ImportMember() + if !imported { + r.ctx.Diagnostics.AddError(diagnostics.ErrUndefinedSymbol, "unsupported qualified path `"+node.TypeText()+"`", ast.LocOf(node), "qualified values currently use `module::member`") + return false + } + if !allowTypeArguments && len(node.Segments[1].TypeArgs) != 0 { + r.ctx.Diagnostics.AddError(diagnostics.ErrInvalidType, "type arguments are not allowed on value paths", ast.LocOf(node), "generic functions and values are not supported") + return false + } + qualifier := qualifierNode.Name + member := memberNode.Name resolved, ok := project.LookupImportedSymbol(r.ctx, r.module, qualifier, member) if !ok || resolved.Symbol == nil { if r.ctx != nil { diff --git a/internal/semantics/typechecker/assignability.go b/internal/semantics/typechecker/assignability.go index 51a6227c..086de8ad 100644 --- a/internal/semantics/typechecker/assignability.go +++ b/internal/semantics/typechecker/assignability.go @@ -286,7 +286,11 @@ func (c *checker) qualifiedScopeType(node *ast.ScopeResolution) typeinfo.Type { sym = c.module.Semantics.ResolvedSymbols[node.ID()] } if sym == nil { - resolved, ok := project.LookupImportedSymbol(c.ctx, c.module, node.Module.Name, node.Name.Name) + qualifier, member, imported := node.ImportValueMember() + if !imported { + return &typeinfo.InvalidType{} + } + resolved, ok := project.LookupImportedSymbol(c.ctx, c.module, qualifier.Name, member.Name) if !ok || resolved.Symbol == nil { return &typeinfo.InvalidType{} } diff --git a/internal/semantics/typechecker/check_call.go b/internal/semantics/typechecker/check_call.go index 9cf2721b..75abd85e 100644 --- a/internal/semantics/typechecker/check_call.go +++ b/internal/semantics/typechecker/check_call.go @@ -401,8 +401,11 @@ func (c *checker) callableSymbol(callee ast.Expr) *symbols.Symbol { return c.module.Semantics.ResolvedSymbols[node.ID()] } case *ast.ScopeResolution: - if resolved, ok := project.LookupImportedSymbol(c.ctx, c.module, node.Module.Name, node.Name.Name); ok { - return resolved.Symbol + qualifier, member, imported := node.ImportValueMember() + if imported { + if resolved, ok := project.LookupImportedSymbol(c.ctx, c.module, qualifier.Name, member.Name); ok { + return resolved.Symbol + } } } return nil @@ -413,8 +416,10 @@ func (c *checker) callableModule(callee ast.Expr) *project.Module { return nil } if node, ok := callee.(*ast.ScopeResolution); ok && node != nil { - if resolved, ok := project.LookupImportedSymbol(c.ctx, c.module, node.Module.Name, node.Name.Name); ok && resolved.Module != nil { - return resolved.Module + if qualifier, member, imported := node.ImportValueMember(); imported { + if resolved, ok := project.LookupImportedSymbol(c.ctx, c.module, qualifier.Name, member.Name); ok && resolved.Module != nil { + return resolved.Module + } } } return c.module diff --git a/internal/semantics/typechecker/check_fn.go b/internal/semantics/typechecker/check_fn.go index 33c9814f..2adc4b0e 100644 --- a/internal/semantics/typechecker/check_fn.go +++ b/internal/semantics/typechecker/check_fn.go @@ -116,7 +116,7 @@ func (c *checker) checkFunctionShape(decl *ast.FnDecl) { } opts := project.TypeSyntaxOptions(c.ctx, c.module, nil, false) fnType := typeinfo.FuncTypeFromDeclWithOptions(decl, opts) - if !c.checkCallableReturn(decl.ReturnType, decl, fnType, decl.ReturnOrigins) { + if !c.checkCallableReturn(decl.ReturnType, decl, fnType, decl.ReturnOrigins, false) { return } for _, param := range decl.ParamsWithReceiver() { @@ -139,7 +139,7 @@ func (c *checker) checkFunctionShape(decl *ast.FnDecl) { } } -func (c *checker) checkCallableReturn(typeNode ast.TypeExpr, fallback ast.Node, fnType *typeinfo.FuncType, clause *ast.ReturnOriginClause) bool { +func (c *checker) checkCallableReturn(typeNode ast.TypeExpr, fallback ast.Node, fnType *typeinfo.FuncType, clause *ast.ReturnOriginClause, allowTypeParameters bool) bool { if fnType == nil { return false } @@ -222,7 +222,7 @@ func (c *checker) checkCallableReturn(typeNode ast.TypeExpr, fallback ast.Node, if c.rejectUnsizedType(typ, typeNode, "function return") { return false } - if !typeinfo.IsLowerableType(typ) { + if !typeinfo.IsLowerableType(typ) && !(allowTypeParameters && typeinfo.ContainsTypeParameter(typ)) { c.ctx.Diagnostics.AddError(diagnostics.ErrInvalidReturn, "function return type is not lowerable in current compiler stage", site, "") return false @@ -231,15 +231,20 @@ func (c *checker) checkCallableReturn(typeNode ast.TypeExpr, fallback ast.Node, } func (c *checker) checkFunctionTypeContracts() { - opts := project.TypeSyntaxOptions(c.ctx, c.module, nil, false) ast.ForEachDecl(c.module.AST, func(decl ast.Decl) bool { + opts := project.TypeSyntaxOptions(c.ctx, c.module, nil, false) + allowTypeParameters := false + if typeDecl, ok := decl.(ast.TypeDecl); ok && len(typeDecl.DeclarationTypeParams()) > 0 { + opts = c.typeDeclSyntaxOptions(typeDecl, false) + allowTypeParameters = true + } ast.Inspect(decl, func(node ast.Node) bool { fnTypeSyntax, ok := node.(*ast.FuncType) if !ok || fnTypeSyntax == nil { return true } fnType, _ := typeinfo.TypeFromSyntax(fnTypeSyntax, opts).(*typeinfo.FuncType) - c.checkCallableReturn(fnTypeSyntax.Return, fnTypeSyntax, fnType, fnTypeSyntax.ReturnOrigins) + c.checkCallableReturn(fnTypeSyntax.Return, fnTypeSyntax, fnType, fnTypeSyntax.ReturnOrigins, allowTypeParameters) return true }) return true @@ -250,7 +255,7 @@ func (c *checker) checkTypeDeclReferenceStorage(decl ast.TypeDecl) { if decl == nil { return } - opts := project.TypeSyntaxOptions(c.ctx, c.module, nil, false) + opts := c.typeDeclSyntaxOptions(decl, false) switch node := decl.(type) { case *ast.StructDecl: strct, ok := node.Type.(*ast.StructType) @@ -279,13 +284,14 @@ func (c *checker) checkInterfaceDecl(decl *ast.InterfaceDecl) { c.ctx.Diagnostics.AddError(diagnostics.ErrInvalidTypeInParser, "interface declaration missing interface payload", ast.LocOf(decl), "") return } - resolvedIface, _ := typeinfo.TypeFromSyntax(iface, project.TypeSyntaxOptions(c.ctx, c.module, nil, false)).(*typeinfo.InterfaceType) + resolvedIface, _ := typeinfo.TypeFromSyntax(iface, c.typeDeclSyntaxOptions(decl, false)).(*typeinfo.InterfaceType) + allowTypeParameters := len(decl.DeclarationTypeParams()) > 0 for methodIndex, method := range iface.Methods { if method.Name == nil || method.Name.Name == "" { c.ctx.Diagnostics.AddError(diagnostics.ErrMissingIdentifier, "interface method name required", method.Location, "") continue } - receiverOpts := project.TypeSyntaxOptions(c.ctx, c.module, nil, true) + receiverOpts := c.typeDeclSyntaxOptions(decl, true) if method.Receiver == nil { c.ctx.Diagnostics.Add(invalidTypeError(method.Name, "iface methods require Self, &Self, or &mut Self receiver")) @@ -299,7 +305,7 @@ func (c *checker) checkInterfaceDecl(decl *ast.InterfaceDecl) { c.ctx.Diagnostics.Add(invalidTypeError(method.Receiver.Type, "iface method receiver must be Self, &Self, or &mut Self")) } - opts := project.TypeSyntaxOptions(c.ctx, c.module, nil, false) + opts := c.typeDeclSyntaxOptions(decl, false) for _, param := range method.Params { paramType := typeinfo.TypeFromSyntax(param.Type, opts) if c.rejectUnsizedType(paramType, param.Type, "interface method parameter") { @@ -308,7 +314,8 @@ func (c *checker) checkInterfaceDecl(decl *ast.InterfaceDecl) { if c.rejectReferenceStorage(paramType, param.Type, "interface parameter aggregate types", false) { continue } - if paramType != nil && !typeinfo.IsLowerableType(paramType) { + if paramType != nil && !typeinfo.IsLowerableType(paramType) && + !(allowTypeParameters && typeinfo.ContainsTypeParameter(paramType)) { site := ast.Node(decl) if param.Name != nil { site = param.Name @@ -318,12 +325,28 @@ func (c *checker) checkInterfaceDecl(decl *ast.InterfaceDecl) { } } if resolvedIface != nil && methodIndex < len(resolvedIface.Methods) { - c.checkCallableReturn(method.ReturnType, decl, resolvedIface.Methods[methodIndex].CallableType(), method.ReturnOrigins) + c.checkCallableReturn(method.ReturnType, decl, resolvedIface.Methods[methodIndex].CallableType(), method.ReturnOrigins, allowTypeParameters) } } } +func (c *checker) typeDeclSyntaxOptions(decl ast.TypeDecl, allowAbstractSelf bool) typeinfo.SyntaxOptions { + opts := project.TypeSyntaxOptions(c.ctx, c.module, nil, allowAbstractSelf) + if c == nil || c.module == nil || c.module.ModuleScope == nil || decl == nil || decl.DeclName() == nil { + return opts + } + sym, ok := c.module.ModuleScope.LookupLocal(decl.DeclName().Name) + if !ok || sym == nil { + return opts + } + defined, ok := sym.Type.(*typeinfo.DefinedType) + if ok && defined != nil { + opts.TypeParameters = typeinfo.TypeParameterBindings(defined.TypeParameters, nil) + } + return opts +} + func (c *checker) checkReceiverFunction(fn *ast.FnDecl) { if c == nil || c.module == nil || fn == nil || fn.Receiver == nil { return diff --git a/internal/semantics/typechecker/typechecker_test.go b/internal/semantics/typechecker/typechecker_test.go index 55ee3a7b..270562cf 100644 --- a/internal/semantics/typechecker/typechecker_test.go +++ b/internal/semantics/typechecker/typechecker_test.go @@ -2265,6 +2265,23 @@ fn main() -> i32 { } } +func TestGenericNamedTypesReachConcreteTypechecking(t *testing.T) { + src := `struct Box { value: T } +type Maybe = ?T; +iface Reader { fn (&Self) read() -> T } + +fn Read(box: &Box) -> i32 { return box.value; } +fn main() -> i32 { + let box: Box = .{ value = 42 }; + let maybe: Maybe = box.value; + return Read(&box); +}` + diag := checkTypeSource(t, src) + if diag.HasErrors() { + t.Fatalf("unexpected generic type diagnostics:\n%s", diag.EmitAllToString()) + } +} + func TestTypedStructLiteralInfersNamedStruct(t *testing.T) { src := `struct Point { x: i32, diff --git a/internal/semantics/typeinfo/capabilities.go b/internal/semantics/typeinfo/capabilities.go index 671aade3..eb6a1f02 100644 --- a/internal/semantics/typeinfo/capabilities.go +++ b/internal/semantics/typeinfo/capabilities.go @@ -109,7 +109,7 @@ func IsSizedType(t Type) bool { switch typ := Underlying(current).(type) { case *InvalidType, *UnknownType, *InterfaceType: return false - case *IntegerType, *ByteType, *CharType, *FloatType, *BoolType, *CStrType, *StringType, *NoneType, *NamedType, *EnumType, *AllocatorType: + case *IntegerType, *ByteType, *CharType, *FloatType, *BoolType, *CStrType, *StringType, *NoneType, *NamedType, *TypeParameterType, *EnumType, *AllocatorType: return true case *OwnedPtrType: return typ != nil && typ.Target != nil diff --git a/internal/semantics/typeinfo/compatibility.go b/internal/semantics/typeinfo/compatibility.go index 08f45cda..871105bf 100644 --- a/internal/semantics/typeinfo/compatibility.go +++ b/internal/semantics/typeinfo/compatibility.go @@ -269,6 +269,12 @@ func sameReturnOriginContract(left, right *ReturnOriginContract) bool { } func checkEnumCompatibility(dst, src Type) Compatibility { + if same, nominal := sameNominalEnum(dst, src); nominal { + if same { + return Compatible + } + return Incompatible + } left, ok := Underlying(dst).(*EnumType) if !ok || left == nil { return Incompatible diff --git a/internal/semantics/typeinfo/relations.go b/internal/semantics/typeinfo/relations.go index fecc355e..113fecee 100644 --- a/internal/semantics/typeinfo/relations.go +++ b/internal/semantics/typeinfo/relations.go @@ -10,6 +10,9 @@ func SameType(left, right Type) bool { if left == right { return true } + if same, nominal := sameNominalEnum(left, right); nominal { + return same + } left = Underlying(left) right = Underlying(right) switch l := left.(type) { @@ -49,6 +52,9 @@ func SameType(left, right Type) bool { case *NamedType: r, ok := right.(*NamedType) return ok && r != nil && l.Name == r.Name + case *TypeParameterType: + r, ok := right.(*TypeParameterType) + return ok && r != nil && l.OwnerIdentity == r.OwnerIdentity && l.Index == r.Index case *OwnedPtrType: r, ok := right.(*OwnedPtrType) return ok && r != nil && SameType(l.Target, r.Target) @@ -77,6 +83,32 @@ func SameType(left, right Type) bool { } } +func sameNominalEnum(left, right Type) (same, nominal bool) { + leftIdentity, leftNominal := nominalEnumIdentity(left) + rightIdentity, rightNominal := nominalEnumIdentity(right) + if !leftNominal && !rightNominal { + return false, false + } + return leftNominal && rightNominal && leftIdentity != "" && leftIdentity == rightIdentity, true +} + +func nominalEnumIdentity(typ Type) (string, bool) { + for { + defined, ok := typ.(*DefinedType) + if !ok || defined == nil { + return "", false + } + switch defined.Kind { + case DefinedKindAlias: + typ = defined.Underlying + case DefinedKindEnum: + return defined.Identity, true + default: + return "", false + } + } +} + type NumericFamily int const ( @@ -181,6 +213,13 @@ func ContainsAbstractSelf(t Type) bool { }) } +func ContainsTypeParameter(t Type) bool { + return containsType(t, typeTraversal{followDefined: true, followCallable: true}, func(candidate Type, _ bool) bool { + _, ok := candidate.(*TypeParameterType) + return ok + }) +} + func ContainsReference(t Type) bool { return containsType(t, typeTraversal{followDefined: true}, func(candidate Type, _ bool) bool { _, ok := candidate.(*RefType) diff --git a/internal/semantics/typeinfo/syntax.go b/internal/semantics/typeinfo/syntax.go index bf3296bc..53cb8641 100644 --- a/internal/semantics/typeinfo/syntax.go +++ b/internal/semantics/typeinfo/syntax.go @@ -8,13 +8,16 @@ import ( ) type SyntaxOptions struct { - Target target.Info - SelfType Type - AllowAbstractSelf bool - ResolveNamed func(name string) (Type, bool) - ResolveQualified func(moduleName, memberName string) (Type, bool) - InvalidSelf func(node *ast.NamedType) Type - InvalidArrayLen func(node *ast.NumberLit) Type + Target target.Info + SelfType Type + AllowAbstractSelf bool + TypeParameters map[string]Type + ResolveNamed func(name string) (Type, bool) + ResolveQualified func(moduleName, memberName string) (Type, bool) + Instantiate func(base *DefinedType, arguments []Type, node ast.TypeExpr) Type + InvalidSelf func(node *ast.NamedType) Type + InvalidArrayLen func(node *ast.NumberLit) Type + InvalidApplication func(node ast.TypeExpr, name string, want, got int) Type } func TypeFromSyntax(node ast.TypeExpr, opts SyntaxOptions) Type { @@ -41,43 +44,37 @@ func TypeFromSyntax(node ast.TypeExpr, opts SyntaxOptions) Type { } return &InvalidType{} } - if opts.ResolveNamed != nil { - if resolved, ok := opts.ResolveNamed(typ.Name); ok && resolved != nil { - return resolved - } + if parameter := opts.TypeParameters[typ.Name]; parameter != nil { + return parameter + } + return applyTypeArguments(typ, resolveTypeName(typ.Name, opts), nil, opts) + case *ast.AppliedType: + if typ == nil || typ.Name == nil { + return nil } - switch typ.Name { - case "bool": - return &BoolType{} - case "byte": - return &ByteType{} - case "char": - return &CharType{} - case "cstr": - return &CStrType{} - case "str", "string": - return &StringType{} - case "f32": - return &FloatType{Bits: 32} - case "f64": - return &FloatType{Bits: 64} - case "Allocator": - return &AllocatorType{} + arguments := make([]Type, len(typ.TypeArgs)) + for index, argument := range typ.TypeArgs { + arguments[index] = TypeFromSyntax(argument, opts) } - if signed, bits, ok := token.ParseIntegerBuiltin(typ.Name, opts.Target); ok { - return &IntegerType{Signed: signed, Bits: bits} + if opts.TypeParameters[typ.Name.Name] != nil { + return applyTypeArguments(typ, &NamedType{Name: typ.Name.Name}, arguments, opts) } - return &NamedType{Name: typ.Name} + return applyTypeArguments(typ, resolveTypeName(typ.Name.Name, opts), arguments, opts) case *ast.ScopeResolution: if typ == nil { return nil } - if opts.ResolveQualified != nil { - if resolved, ok := opts.ResolveQualified(typ.Module.Name, typ.Name.Name); ok && resolved != nil { - return resolved + qualifier, member, imported := typ.ImportMember() + if imported && opts.ResolveQualified != nil { + if resolved, ok := opts.ResolveQualified(qualifier.Name, member.Name); ok && resolved != nil { + arguments := make([]Type, len(typ.Segments[1].TypeArgs)) + for index, argument := range typ.Segments[1].TypeArgs { + arguments[index] = TypeFromSyntax(argument, opts) + } + return applyTypeArguments(typ, resolved, arguments, opts) } } - return &NamedType{Name: typ.Module.Name + "::" + typ.Name.Name} + return &NamedType{Name: typ.TypeText()} case *ast.OwnedPtrType: if typ == nil { return nil @@ -227,6 +224,59 @@ func TypeFromSyntax(node ast.TypeExpr, opts SyntaxOptions) Type { } } +func resolveTypeName(name string, opts SyntaxOptions) Type { + if opts.ResolveNamed != nil { + if resolved, ok := opts.ResolveNamed(name); ok && resolved != nil { + return resolved + } + } + switch name { + case "bool": + return &BoolType{} + case "byte": + return &ByteType{} + case "char": + return &CharType{} + case "cstr": + return &CStrType{} + case "str", "string": + return &StringType{} + case "f32": + return &FloatType{Bits: 32} + case "f64": + return &FloatType{Bits: 64} + case "Allocator": + return &AllocatorType{} + } + if signed, bits, ok := token.ParseIntegerBuiltin(name, opts.Target); ok { + return &IntegerType{Signed: signed, Bits: bits} + } + return &NamedType{Name: name} +} + +func applyTypeArguments(node ast.TypeExpr, base Type, arguments []Type, opts SyntaxOptions) Type { + defined, named := base.(*DefinedType) + want := 0 + if named && defined != nil { + want = len(defined.TypeParameters) + } + got := len(arguments) + if want != got || got > 0 && !named { + name := TypeText(base) + if opts.InvalidApplication != nil { + return opts.InvalidApplication(node, name, want, got) + } + return &InvalidType{} + } + if got == 0 { + return base + } + if opts.Instantiate != nil { + return opts.Instantiate(defined, arguments, node) + } + return &InvalidType{} +} + func FuncTypeFromDeclWithOptions(decl *ast.FnDecl, opts SyntaxOptions) *FuncType { if decl == nil { return nil diff --git a/internal/semantics/typeinfo/types.go b/internal/semantics/typeinfo/types.go index 2164e918..5c80e1ba 100644 --- a/internal/semantics/typeinfo/types.go +++ b/internal/semantics/typeinfo/types.go @@ -41,10 +41,29 @@ type NamedType struct { Name string } +type DefinedKind uint8 + +const ( + DefinedKindInvalid DefinedKind = iota + DefinedKindAlias + DefinedKindStruct + DefinedKindInterface + DefinedKindEnum +) + +type TypeParameterType struct { + Name string + OwnerIdentity string + Index int +} + type DefinedType struct { - Name string - Identity string - Underlying Type + Name string + Identity string + Kind DefinedKind + TypeParameters []*TypeParameterType + TypeArguments []Type + Underlying Type } type OwnedPtrType struct { @@ -145,28 +164,29 @@ type EnumType struct { Variants []string } -func (*InvalidType) TypeNode() {} -func (*UnknownType) TypeNode() {} -func (*IntegerType) TypeNode() {} -func (*ByteType) TypeNode() {} -func (*CharType) TypeNode() {} -func (*FloatType) TypeNode() {} -func (*BoolType) TypeNode() {} -func (*CStrType) TypeNode() {} -func (*StringType) TypeNode() {} -func (*NoneType) TypeNode() {} -func (*AllocatorType) TypeNode() {} -func (*NamedType) TypeNode() {} -func (*DefinedType) TypeNode() {} -func (*OwnedPtrType) TypeNode() {} -func (*RawPtrType) TypeNode() {} -func (*RefType) TypeNode() {} -func (*OptionalType) TypeNode() {} -func (*ArrayType) TypeNode() {} -func (*FuncType) TypeNode() {} -func (*StructType) TypeNode() {} -func (*InterfaceType) TypeNode() {} -func (*EnumType) TypeNode() {} +func (*InvalidType) TypeNode() {} +func (*UnknownType) TypeNode() {} +func (*IntegerType) TypeNode() {} +func (*ByteType) TypeNode() {} +func (*CharType) TypeNode() {} +func (*FloatType) TypeNode() {} +func (*BoolType) TypeNode() {} +func (*CStrType) TypeNode() {} +func (*StringType) TypeNode() {} +func (*NoneType) TypeNode() {} +func (*AllocatorType) TypeNode() {} +func (*NamedType) TypeNode() {} +func (*TypeParameterType) TypeNode() {} +func (*DefinedType) TypeNode() {} +func (*OwnedPtrType) TypeNode() {} +func (*RawPtrType) TypeNode() {} +func (*RefType) TypeNode() {} +func (*OptionalType) TypeNode() {} +func (*ArrayType) TypeNode() {} +func (*FuncType) TypeNode() {} +func (*StructType) TypeNode() {} +func (*InterfaceType) TypeNode() {} +func (*EnumType) TypeNode() {} func (*InvalidType) Text() string { return "" } func (*UnknownType) Text() string { return "" } @@ -209,13 +229,52 @@ func (t *NamedType) Text() string { return t.Name } -func (t *DefinedType) Text() string { +func (t *TypeParameterType) Text() string { if t == nil { return "" } return t.Name } +func (t *DefinedType) Text() string { + if t == nil { + return "" + } + arguments := t.TypeArguments + if len(arguments) == 0 && len(t.TypeParameters) > 0 { + arguments = make([]Type, len(t.TypeParameters)) + for index, parameter := range t.TypeParameters { + arguments[index] = parameter + } + } + if len(arguments) == 0 { + return t.Name + } + parts := make([]string, len(arguments)) + for index, argument := range arguments { + parts[index] = TypeText(argument) + } + return t.Name + "<" + strings.Join(parts, ", ") + ">" +} + +// TypeParameterBindings is the canonical substitution environment for one +// named declaration. Nil arguments retain declaration parameters; concrete +// arguments replace them during instance construction. +func TypeParameterBindings(parameters []*TypeParameterType, arguments []Type) map[string]Type { + bindings := make(map[string]Type, len(parameters)) + for index, parameter := range parameters { + if parameter == nil || parameter.Name == "" { + continue + } + bound := Type(parameter) + if len(arguments) == len(parameters) && arguments[index] != nil { + bound = arguments[index] + } + bindings[parameter.Name] = bound + } + return bindings +} + func Underlying(t Type) Type { for { defined, ok := t.(*DefinedType) @@ -231,13 +290,15 @@ func Underlying(t Type) Type { // representation, while optionals remain structural source types. func VariantDescriptorOf(t Type) (VariantDescriptor, bool) { identity := "" - if defined, ok := t.(*DefinedType); ok && defined != nil { + if enumIdentity, nominal := nominalEnumIdentity(t); nominal { + identity = enumIdentity + } else if defined, ok := t.(*DefinedType); ok && defined != nil && defined.Kind != DefinedKindAlias { identity = defined.Identity if identity == "" { identity = defined.Name } - t = defined.Underlying } + t = Underlying(t) switch variant := t.(type) { case *OptionalType: if variant == nil || variant.Inner == nil { diff --git a/internal/semantics/typeinfo/types_test.go b/internal/semantics/typeinfo/types_test.go index e4672db4..271a48b7 100644 --- a/internal/semantics/typeinfo/types_test.go +++ b/internal/semantics/typeinfo/types_test.go @@ -159,6 +159,10 @@ func TestSizedTypesDistinguishInterfaceCarriers(t *testing.T) { if !IsSizedType(&NamedType{Name: "T"}) { t.Fatalf("generic type parameter must be sized") } + parameter := &TypeParameterType{Name: "T", OwnerIdentity: "Box", Index: 0} + if !IsSizedType(parameter) || IsLowerableType(parameter) { + t.Fatal("declared type parameter must be sized but not backend-lowerable before substitution") + } if IsSizedType(&ArrayType{Shape: ArrayOwner, Elem: iface}) { t.Fatalf("dynamic array cannot contain unsized interface elements") } @@ -431,3 +435,40 @@ func TestVariantDescriptorUnifiesOptionalAndNamedEnumCases(t *testing.T) { t.Fatalf("named descriptor = %#v", named) } } + +func TestNamedEnumCompatibilityUsesDeclarationAndArguments(t *testing.T) { + variants := []string{"Ready", "Waiting"} + left := &DefinedType{ + Name: "Status", Identity: "left::Status", Kind: DefinedKindEnum, + Underlying: &EnumType{Variants: variants}, + } + right := &DefinedType{ + Name: "Status", Identity: "right::Status", Kind: DefinedKindEnum, + Underlying: &EnumType{Variants: variants}, + } + leftAgain := &DefinedType{ + Name: "Status", Identity: "left::Status", Kind: DefinedKindEnum, + Underlying: &EnumType{Variants: variants}, + } + if SameType(left, right) || Assignable(left, right) { + t.Fatal("different enum declarations must remain nominally distinct") + } + if !SameType(left, leftAgain) || !Assignable(left, leftAgain) { + t.Fatal("same enum declaration and arguments must be compatible") + } +} + +func TestVariantDescriptorUsesEnumIdentityThroughTransparentAlias(t *testing.T) { + status := &DefinedType{ + Name: "Status", Identity: "module::Status", Kind: DefinedKindEnum, + Underlying: &EnumType{Variants: []string{"Ready", "Waiting"}}, + } + alias := &DefinedType{ + Name: "State", Identity: "module::State", Kind: DefinedKindAlias, + Underlying: status, + } + descriptor, ok := VariantDescriptorOf(alias) + if !ok || descriptor.Identity != status.Identity || descriptor.Family != VariantFamilyNamed { + t.Fatalf("aliased enum descriptor = %#v, want identity %q", descriptor, status.Identity) + } +} diff --git a/x_test/negative_generic_type_arity/peeper.toml b/x_test/negative_generic_type_arity/peeper.toml new file mode 100644 index 00000000..8cee677e --- /dev/null +++ b/x_test/negative_generic_type_arity/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_generic_type_arity" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0021", "expects 1 type argument, got 0"] diff --git a/x_test/negative_generic_type_arity/src/main.peep b/x_test/negative_generic_type_arity/src/main.peep new file mode 100644 index 00000000..386356d8 --- /dev/null +++ b/x_test/negative_generic_type_arity/src/main.peep @@ -0,0 +1,5 @@ +struct Box { + value: T +} + +fn Use(value: Box) {} diff --git a/x_test/negative_imported_value_type_arguments/peeper.toml b/x_test/negative_imported_value_type_arguments/peeper.toml new file mode 100644 index 00000000..0e2f2798 --- /dev/null +++ b/x_test/negative_imported_value_type_arguments/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_imported_value_type_arguments" +build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0021", "type arguments are not allowed on value paths"] diff --git a/x_test/negative_imported_value_type_arguments/src/main.peep b/x_test/negative_imported_value_type_arguments/src/main.peep new file mode 100644 index 00000000..15722bbf --- /dev/null +++ b/x_test/negative_imported_value_type_arguments/src/main.peep @@ -0,0 +1,5 @@ +import "negative_imported_value_type_arguments/runtime"; + +fn main() -> i32 { + return runtime::Make(); +} diff --git a/x_test/negative_imported_value_type_arguments/src/runtime.peep b/x_test/negative_imported_value_type_arguments/src/runtime.peep new file mode 100644 index 00000000..b4087fa9 --- /dev/null +++ b/x_test/negative_imported_value_type_arguments/src/runtime.peep @@ -0,0 +1,3 @@ +fn Make() -> i32 { + return 42; +} diff --git a/x_test/negative_nongeneric_type_arguments/peeper.toml b/x_test/negative_nongeneric_type_arguments/peeper.toml new file mode 100644 index 00000000..2c684630 --- /dev/null +++ b/x_test/negative_nongeneric_type_arguments/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_nongeneric_type_arguments" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0021", "expects 0 type arguments, got 1"] diff --git a/x_test/negative_nongeneric_type_arguments/src/main.peep b/x_test/negative_nongeneric_type_arguments/src/main.peep new file mode 100644 index 00000000..e073e6fe --- /dev/null +++ b/x_test/negative_nongeneric_type_arguments/src/main.peep @@ -0,0 +1,3 @@ +struct Plain {} + +fn Use(value: Plain) {} diff --git a/x_test/runtime_generic_named_types/peeper.toml b/x_test/runtime_generic_named_types/peeper.toml new file mode 100644 index 00000000..d606ae49 --- /dev/null +++ b/x_test/runtime_generic_named_types/peeper.toml @@ -0,0 +1,7 @@ +name = "runtime_generic_named_types" +build = "program" + +[test] +mode = "run" +outcome = "exit_code" +exit_code = 42 diff --git a/x_test/runtime_generic_named_types/src/main.peep b/x_test/runtime_generic_named_types/src/main.peep new file mode 100644 index 00000000..d8735eb6 --- /dev/null +++ b/x_test/runtime_generic_named_types/src/main.peep @@ -0,0 +1,15 @@ +struct Box { + value: T +} + +type Maybe = ?T; + +fn main() -> i32 { + let inner: Box = .{ value = 42 }; + let outer: Box> = .{ value = inner }; + let maybe: Maybe = outer.value.value; + if maybe != none { + return maybe; + } + return 1; +} diff --git a/x_test/type_imported_generic/peeper.toml b/x_test/type_imported_generic/peeper.toml new file mode 100644 index 00000000..a07fb26f --- /dev/null +++ b/x_test/type_imported_generic/peeper.toml @@ -0,0 +1,6 @@ +name = "type_imported_generic" +build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_imported_generic/src/container.peep b/x_test/type_imported_generic/src/container.peep new file mode 100644 index 00000000..0e2395a2 --- /dev/null +++ b/x_test/type_imported_generic/src/container.peep @@ -0,0 +1,3 @@ +struct Box { + value: T +} diff --git a/x_test/type_imported_generic/src/main.peep b/x_test/type_imported_generic/src/main.peep new file mode 100644 index 00000000..a878b0cb --- /dev/null +++ b/x_test/type_imported_generic/src/main.peep @@ -0,0 +1,10 @@ +import "type_imported_generic/container"; + +fn Read(box: &container::Box) -> i32 { + return box.value; +} + +fn main() -> i32 { + let box: container::Box = .{ value = 9 }; + return Read(&box); +} From ed748354cdc380280c9d6491be683c76e251abb5 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Mon, 24 Aug 2026 22:39:00 +0600 Subject: [PATCH 2/2] Harden generic type instance lifecycle Persist collected generic declarations on modules so fresh incremental contexts can reindex them. Canonicalize transparent arguments, permit exact recursive shells, and reject argument-changing recursion without poisoning the instance cache. Unalias centralizes transparent semantic normalization. ContainsInvalid reuses the cycle-safe type graph walker. finishTypeInstance owns provisional publication, invalidation, and waiter wakeup. --- docs/language-spec.md | 22 +++ docs/ownership-pointer-model.md | 3 + internal/lsp/workspace_test.go | 30 +++++ internal/project/context.go | 13 +- internal/project/generic_types.go | 127 +++++++++++++++--- internal/project/modules.go | 11 ++ internal/project/modules_test.go | 26 +++- internal/project/type_syntax.go | 2 +- internal/semantics/binder/binder_test.go | 90 +++++++++++++ internal/semantics/typeinfo/relations.go | 6 + internal/semantics/typeinfo/types.go | 17 +++ internal/semantics/typeinfo/types_test.go | 20 +++ .../peeper.toml | 7 + .../src/main.peep | 5 + .../peeper.toml | 7 + .../src/main.peep | 5 + .../type_generic_alias_identity/peeper.toml | 6 + .../type_generic_alias_identity/src/main.peep | 13 ++ 18 files changed, 388 insertions(+), 22 deletions(-) create mode 100644 x_test/negative_expanding_generic_recursion/peeper.toml create mode 100644 x_test/negative_expanding_generic_recursion/src/main.peep create mode 100644 x_test/negative_transformed_generic_recursion/peeper.toml create mode 100644 x_test/negative_transformed_generic_recursion/src/main.peep create mode 100644 x_test/type_generic_alias_identity/peeper.toml create mode 100644 x_test/type_generic_alias_identity/src/main.peep diff --git a/docs/language-spec.md b/docs/language-spec.md index c37e4c89..62acb948 100644 --- a/docs/language-spec.md +++ b/docs/language-spec.md @@ -73,6 +73,28 @@ must be ordered, within the byte length, and on UTF-8 codepoint boundaries. Invalid bounds or boundaries trap at runtime. The owner remains responsible for backing storage and is dropped exactly once. +## Generic Named Types + +Structs, enums, interfaces, and transparent type aliases may declare type +parameters. Every use supplies exact explicit arguments, including imported and +nested applications such as `container::Box>`. Type-context +parsing splits adjacent closing `>>` tokens; expression comparisons keep normal +`<` and `>` behavior. + +Transparent aliases do not create a new generic argument identity. If +`MyInt = i32`, then `Choice` and `Choice` are the same semantic +instance. Nominal identity still belongs to the applied declaration and its +canonical arguments. + +Recursive generic applications must preserve exact canonical arguments. +`Node` may contain `*Node` or another fixed-size indirect reference to the +same application. Expanding or transformed recursion such as +`Loop -> Loop>` or `Pair -> Pair` is rejected. Direct +by-value recursion remains unsized and rejected independently. + +Constraints, defaults, generic inference, generic functions, generic methods, +and monomorphization are not part of current language surface. + ## Optional Values And Flow Narrowing `?T` contains either one `T` value or `none`. `none` is valid only where an diff --git a/docs/ownership-pointer-model.md b/docs/ownership-pointer-model.md index b818fc58..0b30b8ca 100644 --- a/docs/ownership-pointer-model.md +++ b/docs/ownership-pointer-model.md @@ -405,6 +405,9 @@ lowering reserves one identity for each named recursive composite before lowering its children. LLVM emits identified aggregate declarations and one reusable private drop function per owning named composite, so recursive destruction calls the existing function instead of recursively expanding IR. +Generic recursive links must repeat same canonical type arguments. Exact +`Node -> *Node` recursion reuses one semantic and runtime shell; +argument-expanding recursion is rejected before lowering. ## Final Rules diff --git a/internal/lsp/workspace_test.go b/internal/lsp/workspace_test.go index 15706cb4..9db2453f 100644 --- a/internal/lsp/workspace_test.go +++ b/internal/lsp/workspace_test.go @@ -147,6 +147,36 @@ func TestServerStateReusesUnchangedWorkspaceComponent(t *testing.T) { } } +func TestServerStateReindexesReusedGenericDeclarations(t *testing.T) { + root := t.TempDir() + writeWorkspaceProjectConfig(t, root, "app") + entry := filepath.Join(root, peeper.SourceDirName, peeper.MainFileName) + container := filepath.Join(root, peeper.SourceDirName, "container"+peeper.SourceExt) + const initial = `import "app/container"; +fn Take(value: container::Box) {} +fn main() {}` + writeWorkspaceFile(t, entry, initial) + writeWorkspaceFile(t, container, "struct Box { value: T }\n") + + state := NewServerState() + state.RootDir = root + ctx, mod := state.recompile(entry) + if mod == nil || ctx == nil || ctx.Diagnostics.HasErrors() { + t.Fatalf("initial generic compile failed:\n%s", ctx.Diagnostics.EmitAllToString()) + } + + state.Cache[entry] = `import "app/container"; +fn Take(value: container::Box) {} +fn main() { let body_only = 1; }` + ctx, mod = state.recompile(entry) + if mod == nil || ctx == nil { + t.Fatal("incremental generic compile returned nil") + } + if ctx.Diagnostics.HasErrors() { + t.Fatalf("incremental generic compile lost declaration registry:\n%s", ctx.Diagnostics.EmitAllToString()) + } +} + func TestServerStateReplaysDiagnosticsForUnchangedModule(t *testing.T) { root := t.TempDir() writeWorkspaceProjectConfig(t, root, "app") diff --git a/internal/project/context.go b/internal/project/context.go index aac55a57..2ef2560f 100644 --- a/internal/project/context.go +++ b/internal/project/context.go @@ -47,8 +47,8 @@ type CompilerContext struct { fileIndex map[string]string // Prior semantic API fingerprints supplied by incremental clients. semanticExportBaselines map[string]string - // Named declaration identity -> declaration syntax and owning module. - typeDeclarations map[string]namedTypeDeclaration + // Named declaration identity -> collected module declaration index. + typeDeclarations map[string]*Module // Concrete semantic application identity -> canonical instance. typeInstances map[string]namedTypeInstance // Shared compiler dependency graph. @@ -173,7 +173,7 @@ func NewWithConfig(cfg Config, diag *diagnostics.DiagnosticBag) *CompilerContext modules: make(map[string]*Module), fileIndex: make(map[string]string), semanticExportBaselines: make(map[string]string), - typeDeclarations: make(map[string]namedTypeDeclaration), + typeDeclarations: make(map[string]*Module), typeInstances: make(map[string]namedTypeInstance), } } @@ -197,12 +197,15 @@ func (ctx *CompilerContext) ResetModule(module *Module, retained phase.Phase) { ctx.mu.Lock() for identity, instance := range ctx.typeInstances { if instance.ownerModuleKey == module.Key { + if !instance.complete && instance.ready != nil { + close(instance.ready) + } delete(ctx.typeInstances, identity) } } if retained < phase.Collected { - for identity, declaration := range ctx.typeDeclarations { - if declaration.module == module { + for identity, owner := range ctx.typeDeclarations { + if owner != nil && owner.Key == module.Key { delete(ctx.typeDeclarations, identity) } } diff --git a/internal/project/generic_types.go b/internal/project/generic_types.go index 32d93362..c37e571e 100644 --- a/internal/project/generic_types.go +++ b/internal/project/generic_types.go @@ -10,7 +10,6 @@ import ( ) type namedTypeDeclaration struct { - module *Module syntax ast.TypeDecl base *typeinfo.DefinedType } @@ -18,25 +17,46 @@ type namedTypeDeclaration struct { type namedTypeInstance struct { ownerModuleKey string typ *typeinfo.DefinedType + ready chan struct{} + complete bool } -// RegisterTypeDeclaration preserves declaration syntax beside its stable -// semantic shell so concrete applications can substitute from one source. +type typeInstantiationFrame struct { + declarationIdentity string + applicationIdentity string + applicationText string + node ast.TypeExpr +} + +// RegisterTypeDeclaration records collection's reusable declaration artifact +// and indexes it for concrete substitution in the current context. func (ctx *CompilerContext) RegisterTypeDeclaration(module *Module, declaration ast.TypeDecl, base *typeinfo.DefinedType) { if ctx == nil || module == nil || declaration == nil || base == nil || base.Identity == "" { return } + artifact := namedTypeDeclaration{syntax: declaration, base: base} ctx.mu.Lock() - ctx.typeDeclarations[base.Identity] = namedTypeDeclaration{module: module, syntax: declaration, base: base} + if module.namedTypeDeclarations == nil { + module.namedTypeDeclarations = make(map[string]namedTypeDeclaration) + } + module.namedTypeDeclarations[base.Identity] = artifact + ctx.typeDeclarations[base.Identity] = module ctx.mu.Unlock() } -func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, arguments []typeinfo.Type, node ast.TypeExpr) typeinfo.Type { +func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, arguments []typeinfo.Type, node ast.TypeExpr, chain []typeInstantiationFrame) typeinfo.Type { if ctx == nil || base == nil || len(arguments) != len(base.TypeParameters) { return &typeinfo.InvalidType{} } - declarationArguments := true + canonicalArguments := make([]typeinfo.Type, len(arguments)) for index, argument := range arguments { + canonicalArguments[index] = typeinfo.Unalias(argument) + if typeinfo.IsInvalid(canonicalArguments[index]) { + return &typeinfo.InvalidType{} + } + } + declarationArguments := true + for index, argument := range canonicalArguments { if argument != base.TypeParameters[index] { declarationArguments = false break @@ -46,19 +66,59 @@ func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, argument return base } - argumentKeys := make([]string, len(arguments)) - for index, argument := range arguments { + argumentKeys := make([]string, len(canonicalArguments)) + for index, argument := range canonicalArguments { argumentKeys[index] = typeArgumentIdentity(argument) } identity := base.Identity + "<" + strings.Join(argumentKeys, ",") + ">" + applicationText := (&typeinfo.DefinedType{Name: base.Name, TypeArguments: canonicalArguments}).Text() + for _, origin := range chain { + if origin.declarationIdentity != base.Identity { + continue + } + if origin.applicationIdentity == identity { + ctx.mu.RLock() + cached, ok := ctx.typeInstances[identity] + ctx.mu.RUnlock() + if ok && cached.typ != nil { + return cached.typ + } + return &typeinfo.InvalidType{} + } + if ctx.Diagnostics != nil { + diagnostic := diagnostics.NewError("recursive generic applications must preserve exact type arguments"). + WithCode(diagnostics.ErrInvalidType). + WithPrimaryLabel(ast.LocOf(node), "`"+applicationText+"` changes recursive arguments"). + WithSecondaryLabel(ast.LocOf(origin.node), "`"+origin.applicationText+"` started this instantiation"). + WithHelp("use the same canonical type arguments at every recursive reference") + ctx.Diagnostics.Add(diagnostic) + } + return &typeinfo.InvalidType{} + } ctx.mu.Lock() - if cached, ok := ctx.typeInstances[identity]; ok && cached.typ != nil { + if cached, ok := ctx.typeInstances[identity]; ok && cached.typ != nil && cached.complete { ctx.mu.Unlock() return cached.typ } - declaration, ok := ctx.typeDeclarations[base.Identity] - if !ok || declaration.module == nil || declaration.syntax == nil || declaration.base != base { + if cached, ok := ctx.typeInstances[identity]; ok && cached.typ != nil { + ready := cached.ready + ctx.mu.Unlock() + <-ready + ctx.mu.RLock() + cached, ok = ctx.typeInstances[identity] + ctx.mu.RUnlock() + if ok && cached.typ != nil && cached.complete { + return cached.typ + } + return &typeinfo.InvalidType{} + } + declarationModule, ok := ctx.typeDeclarations[base.Identity] + declaration := namedTypeDeclaration{} + if ok && declarationModule != nil { + declaration = declarationModule.namedTypeDeclarations[base.Identity] + } + if !ok || declarationModule == nil || declaration.syntax == nil || declaration.base != base { ctx.mu.Unlock() if ctx.Diagnostics != nil { ctx.Diagnostics.AddError(diagnostics.ErrInvalidType, @@ -71,19 +131,56 @@ func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, argument Identity: identity, Kind: base.Kind, TypeParameters: base.TypeParameters, - TypeArguments: append([]typeinfo.Type(nil), arguments...), + TypeArguments: canonicalArguments, } // Cache provisional shell before substitution. Recursive pointer/reference // applications resolve back to this exact object. - ctx.typeInstances[identity] = namedTypeInstance{ownerModuleKey: declaration.module.Key, typ: instance} + ctx.typeInstances[identity] = namedTypeInstance{ + ownerModuleKey: declarationModule.Key, + typ: instance, + ready: make(chan struct{}), + } ctx.mu.Unlock() - opts := TypeSyntaxOptions(ctx, declaration.module, nil, true) - opts.TypeParameters = typeinfo.TypeParameterBindings(base.TypeParameters, arguments) + chain = append(chain, typeInstantiationFrame{ + declarationIdentity: base.Identity, + applicationIdentity: identity, + applicationText: applicationText, + node: node, + }) + opts := TypeSyntaxOptions(ctx, declarationModule, nil, true) + opts.TypeParameters = typeinfo.TypeParameterBindings(base.TypeParameters, canonicalArguments) + opts.Instantiate = func(nestedBase *typeinfo.DefinedType, nestedArguments []typeinfo.Type, nestedNode ast.TypeExpr) typeinfo.Type { + return ctx.instantiateType(nestedBase, nestedArguments, nestedNode, chain) + } instance.Underlying = typeinfo.TypeFromSyntax(declaration.syntax.UnderlyingType(), opts) + valid := !typeinfo.ContainsInvalid(instance.Underlying) + ctx.finishTypeInstance(identity, instance, valid) + if !valid { + return &typeinfo.InvalidType{} + } return instance } +// finishTypeInstance publishes or removes one provisional cache entry and +// wakes any concurrent application waiting on the same semantic identity. +func (ctx *CompilerContext) finishTypeInstance(identity string, instance *typeinfo.DefinedType, valid bool) { + ctx.mu.Lock() + cached, ok := ctx.typeInstances[identity] + if !ok || cached.typ != instance { + ctx.mu.Unlock() + return + } + if valid { + cached.complete = true + ctx.typeInstances[identity] = cached + } else { + delete(ctx.typeInstances, identity) + } + close(cached.ready) + ctx.mu.Unlock() +} + func typeArgumentIdentity(typ typeinfo.Type) string { switch value := typ.(type) { case *typeinfo.DefinedType: diff --git a/internal/project/modules.go b/internal/project/modules.go index 2bdb4a88..4a4350f5 100644 --- a/internal/project/modules.go +++ b/internal/project/modules.go @@ -76,6 +76,9 @@ type Module struct { LLVMIR string // Top-level names visible in module. ModuleScope *symbols.Scope + // Generic declaration syntax and semantic shells produced by collection. + // Fresh incremental contexts reindex this immutable phase artifact. + namedTypeDeclarations map[string]namedTypeDeclaration // Grouped semantic analysis metadata. Semantics *SemanticInfo // Import alias -> resolved module import. @@ -189,6 +192,9 @@ func (m *Module) resetToPhase(retained phase.Phase) { m.ModuleScope = nil m.Semantics = nil } + if retained < phase.Collected { + m.namedTypeDeclarations = nil + } if retained < phase.Typechecked { m.SemanticExportFingerprint = "" m.TypedASTNodes = nil @@ -270,6 +276,11 @@ func (ctx *CompilerContext) AddModule(module *Module) { if module.FilePath != "" { ctx.fileIndex[CanonicalPath(module.FilePath)] = module.Key } + if module.Phase >= phase.Collected { + for identity := range module.namedTypeDeclarations { + ctx.typeDeclarations[identity] = module + } + } } // Lookup by graph identity. diff --git a/internal/project/modules_test.go b/internal/project/modules_test.go index f66a69c5..9acb1d2a 100644 --- a/internal/project/modules_test.go +++ b/internal/project/modules_test.go @@ -117,7 +117,7 @@ func TestCompilerContextResetPurgesOwnedNamedTypeInstances(t *testing.T) { typ: &typeinfo.DefinedType{Name: "Box", Identity: "other::Box"}, } - ctx.ResetModule(module, phase.Parsed) + ctx.ResetModule(&Module{Key: module.Key}, phase.Parsed) if _, found := ctx.typeInstances["owner::Box"]; found { t.Fatal("reset retained instance owned by reset module") @@ -126,3 +126,27 @@ func TestCompilerContextResetPurgesOwnedNamedTypeInstances(t *testing.T) { t.Fatal("reset removed instance owned by another module") } } + +func TestCompilerContextReindexesCollectedTypeDeclarations(t *testing.T) { + module := &Module{Key: "owner", Phase: phase.Collected} + base := &typeinfo.DefinedType{Name: "Box", Identity: "owner::Box", Kind: typeinfo.DefinedKindStruct} + declaration := &ast.StructDecl{Name: &ast.Ident{Name: "Box"}} + original := New(".", ".peep", nil) + original.RegisterTypeDeclaration(module, declaration, base) + + fresh := New(".", ".peep", nil) + fresh.AddModule(module) + registeredModule, found := fresh.typeDeclarations[base.Identity] + registered := module.namedTypeDeclarations[base.Identity] + if !found || registeredModule != module || registered.base != base || registered.syntax != declaration { + t.Fatalf("reindexed declaration module = %#v, artifact = %#v", registeredModule, registered) + } + + fresh.ResetModule(module, phase.Parsed) + if module.namedTypeDeclarations != nil { + t.Fatal("reset below collection retained module declaration artifact") + } + if _, found := fresh.typeDeclarations[base.Identity]; found { + t.Fatal("reset below collection retained context declaration index") + } +} diff --git a/internal/project/type_syntax.go b/internal/project/type_syntax.go index 834883d3..ce6257e0 100644 --- a/internal/project/type_syntax.go +++ b/internal/project/type_syntax.go @@ -42,7 +42,7 @@ func TypeSyntaxOptions(ctx *CompilerContext, module *Module, selfType typeinfo.T return symbols.GetSymbolType(resolved.Symbol) }, Instantiate: func(base *typeinfo.DefinedType, arguments []typeinfo.Type, node ast.TypeExpr) typeinfo.Type { - return ctx.instantiateType(base, arguments, node) + return ctx.instantiateType(base, arguments, node, nil) }, InvalidSelf: func(node *ast.NamedType) typeinfo.Type { if ctx != nil && ctx.Diagnostics != nil { diff --git a/internal/semantics/binder/binder_test.go b/internal/semantics/binder/binder_test.go index 333df9e8..34e0442a 100644 --- a/internal/semantics/binder/binder_test.go +++ b/internal/semantics/binder/binder_test.go @@ -1,8 +1,12 @@ package binder import ( + "context" + "os" + "os/exec" "strings" "testing" + "time" "compiler/internal/diagnostics" "compiler/internal/frontend/lexer" @@ -176,6 +180,92 @@ fn Use(box: Box, again: Box, other: Box, nested: Box>, n } } +func TestBindCanonicalizesTransparentGenericArguments(t *testing.T) { + const filePath = "binder_generic_alias_identity_test" + peeper.SourceExt + const src = `type BaseInt = i32; +type MyInt = BaseInt; +enum Choice { Left, Right } +fn Use(alias: Choice, canonical: Choice) {}` + diag := diagnostics.NewDiagnosticBag() + ctx := project.New(".", peeper.SourceExt, diag) + module := &project.Module{ + Key: project.ModuleKeyFor(project.ModuleOriginLocal, filePath), + FilePath: filePath, + Content: src, + AST: parser.New(filePath, lexer.New(filePath, src, diag).Tokenize(), diag).ParseModule(), + Imports: make(map[string]project.ResolvedImport), + } + collector.Collect(ctx, module) + Bind(ctx, module) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + + use, ok := module.ModuleScope.LookupLocal("Use") + if !ok || use == nil { + t.Fatal("missing Use function") + } + fn, ok := use.Type.(*typeinfo.FuncType) + if !ok || len(fn.Params) != 2 { + t.Fatalf("Use type = %#v", use.Type) + } + if fn.Params[0] != fn.Params[1] { + t.Fatalf("transparent alias split generic identity: %#v != %#v", fn.Params[0], fn.Params[1]) + } +} + +func TestBindRejectsExpandingGenericRecursion(t *testing.T) { + if os.Getenv("PEEPER_TEST_EXPANDING_GENERIC_RECURSION") == "1" { + tests := []struct { + name string + source string + }{ + { + name: "expanding", + source: `struct Loop { next: *Loop> } +fn Use(value: &Loop) {}`, + }, + { + name: "transformed", + source: `struct Swap { next: *Swap } +fn Use(value: &Swap) {}`, + }, + } + for _, test := range tests { + filePath := "binder_" + test.name + "_generic_recursion_test" + peeper.SourceExt + diag := diagnostics.NewDiagnosticBag() + ctx := project.New(".", peeper.SourceExt, diag) + module := &project.Module{ + Key: project.ModuleKeyFor(project.ModuleOriginLocal, filePath), + FilePath: filePath, + Content: test.source, + AST: parser.New(filePath, lexer.New(filePath, test.source, diag).Tokenize(), diag).ParseModule(), + Imports: make(map[string]project.ResolvedImport), + } + collector.Collect(ctx, module) + Bind(ctx, module) + out := diag.EmitAllToString() + if !diag.HasErrors() || !strings.Contains(out, diagnostics.ErrInvalidType) || + !strings.Contains(out, "recursive generic applications must preserve exact type arguments") { + t.Fatalf("%s: expected regular-recursion diagnostic, got:\n%s", test.name, out) + } + } + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestBindRejectsExpandingGenericRecursion$") + cmd.Env = append(os.Environ(), "PEEPER_TEST_EXPANDING_GENERIC_RECURSION=1") + output, err := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + t.Fatal("expanding generic recursion did not terminate") + } + if err != nil { + t.Fatalf("expanding generic recursion child failed: %v\n%s", err, output) + } +} + func TestBindRequiresExactNamedTypeArguments(t *testing.T) { tests := []struct { name string diff --git a/internal/semantics/typeinfo/relations.go b/internal/semantics/typeinfo/relations.go index 113fecee..7cb78cf3 100644 --- a/internal/semantics/typeinfo/relations.go +++ b/internal/semantics/typeinfo/relations.go @@ -220,6 +220,12 @@ func ContainsTypeParameter(t Type) bool { }) } +func ContainsInvalid(t Type) bool { + return containsType(t, typeTraversal{followDefined: true, followCallable: true}, func(candidate Type, _ bool) bool { + return IsInvalid(candidate) + }) +} + func ContainsReference(t Type) bool { return containsType(t, typeTraversal{followDefined: true}, func(candidate Type, _ bool) bool { _, ok := candidate.(*RefType) diff --git a/internal/semantics/typeinfo/types.go b/internal/semantics/typeinfo/types.go index 5c80e1ba..e69040f7 100644 --- a/internal/semantics/typeinfo/types.go +++ b/internal/semantics/typeinfo/types.go @@ -275,6 +275,23 @@ func TypeParameterBindings(parameters []*TypeParameterType, arguments []Type) ma return bindings } +// Unalias returns canonical transparent-alias storage without erasing nominal +// structs, interfaces, or enums. Invalid alias cycles terminate as invalid. +func Unalias(t Type) Type { + seen := make(map[*DefinedType]struct{}) + for { + defined, ok := t.(*DefinedType) + if !ok || defined == nil || defined.Kind != DefinedKindAlias || defined.Underlying == nil { + return t + } + if _, found := seen[defined]; found { + return &InvalidType{} + } + seen[defined] = struct{}{} + t = defined.Underlying + } +} + func Underlying(t Type) Type { for { defined, ok := t.(*DefinedType) diff --git a/internal/semantics/typeinfo/types_test.go b/internal/semantics/typeinfo/types_test.go index 271a48b7..13d77e8f 100644 --- a/internal/semantics/typeinfo/types_test.go +++ b/internal/semantics/typeinfo/types_test.go @@ -472,3 +472,23 @@ func TestVariantDescriptorUsesEnumIdentityThroughTransparentAlias(t *testing.T) t.Fatalf("aliased enum descriptor = %#v, want identity %q", descriptor, status.Identity) } } + +func TestUnaliasCanonicalizesChainsWithoutErasingNominalTypes(t *testing.T) { + integer := &IntegerType{Signed: true, Bits: 32} + inner := &DefinedType{Name: "Inner", Kind: DefinedKindAlias, Underlying: integer} + outer := &DefinedType{Name: "Outer", Kind: DefinedKindAlias, Underlying: inner} + if got := Unalias(outer); got != integer { + t.Fatalf("Unalias(alias chain) = %#v, want canonical integer", got) + } + + nominal := &DefinedType{Name: "Value", Kind: DefinedKindStruct, Underlying: &StructType{}} + if got := Unalias(nominal); got != nominal { + t.Fatalf("Unalias(nominal) = %#v, want original nominal type", got) + } + + cycle := &DefinedType{Name: "Cycle", Kind: DefinedKindAlias} + cycle.Underlying = cycle + if got := Unalias(cycle); !IsInvalid(got) { + t.Fatalf("Unalias(alias cycle) = %#v, want invalid", got) + } +} diff --git a/x_test/negative_expanding_generic_recursion/peeper.toml b/x_test/negative_expanding_generic_recursion/peeper.toml new file mode 100644 index 00000000..a2ab6465 --- /dev/null +++ b/x_test/negative_expanding_generic_recursion/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_expanding_generic_recursion" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0021", "recursive generic applications must preserve exact type arguments"] diff --git a/x_test/negative_expanding_generic_recursion/src/main.peep b/x_test/negative_expanding_generic_recursion/src/main.peep new file mode 100644 index 00000000..a981871a --- /dev/null +++ b/x_test/negative_expanding_generic_recursion/src/main.peep @@ -0,0 +1,5 @@ +struct Loop { + next: *Loop> +} + +fn Use(value: &Loop) {} diff --git a/x_test/negative_transformed_generic_recursion/peeper.toml b/x_test/negative_transformed_generic_recursion/peeper.toml new file mode 100644 index 00000000..90ba4fb9 --- /dev/null +++ b/x_test/negative_transformed_generic_recursion/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_transformed_generic_recursion" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0021", "recursive generic applications must preserve exact type arguments"] diff --git a/x_test/negative_transformed_generic_recursion/src/main.peep b/x_test/negative_transformed_generic_recursion/src/main.peep new file mode 100644 index 00000000..60abfa49 --- /dev/null +++ b/x_test/negative_transformed_generic_recursion/src/main.peep @@ -0,0 +1,5 @@ +struct Swap { + next: *Swap +} + +fn Use(value: &Swap) {} diff --git a/x_test/type_generic_alias_identity/peeper.toml b/x_test/type_generic_alias_identity/peeper.toml new file mode 100644 index 00000000..99411f4b --- /dev/null +++ b/x_test/type_generic_alias_identity/peeper.toml @@ -0,0 +1,6 @@ +name = "type_generic_alias_identity" +build = "lib" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_generic_alias_identity/src/main.peep b/x_test/type_generic_alias_identity/src/main.peep new file mode 100644 index 00000000..54a1b82d --- /dev/null +++ b/x_test/type_generic_alias_identity/src/main.peep @@ -0,0 +1,13 @@ +type BaseInt = i32; +type MyInt = BaseInt; + +enum Choice { + Left, + Right +} + +fn Accept(value: Choice) {} + +fn Forward(value: Choice) { + Accept(value); +}