From bac1183593fe0af7573cf706140def9a0249863b Mon Sep 17 00:00:00 2001 From: pavel Date: Thu, 27 Aug 2026 21:28:11 +0200 Subject: [PATCH] Move LSP semantics into typed analysis --- README.md | 22 + cmd/gotlin-lsp/README.md | 2 +- cmd/gotlin-lsp/main.go | 579 +++--------------- cmd/gotlin-lsp/main_test.go | 17 +- examples/generics.gt | 15 + examples/http_server.gt | 2 +- go.mod | 10 +- go.sum | 7 + internal/lang/ast.go | 23 +- internal/lang/diagnostics.go | 50 ++ internal/lang/diagnostics_test.go | 37 ++ internal/lang/generate_go.go | 60 +- internal/lang/generics_test.go | 72 +++ internal/lang/go_interop_test.go | 2 +- internal/lang/go_types.go | 26 +- internal/lang/go_types_test.go | 32 + internal/lang/hir.go | 100 ++- internal/lang/parser.go | 67 +- internal/lang/semantics.go | 10 +- internal/lang/symbols.go | 229 ++++--- internal/lang/type_checker.go | 104 +++- internal/lang/type_refs.go | 248 ++++++++ internal/lang/types.go | 144 ++++- internal/lang/types_test.go | 9 + tools/vscode-gotlin/scripts/validate.js | 2 +- .../snippets/gotlin.code-snippets | 16 + 26 files changed, 1240 insertions(+), 645 deletions(-) create mode 100644 examples/generics.gt create mode 100644 internal/lang/diagnostics.go create mode 100644 internal/lang/diagnostics_test.go create mode 100644 internal/lang/generics_test.go create mode 100644 internal/lang/go_types_test.go create mode 100644 internal/lang/type_refs.go diff --git a/README.md b/README.md index 1ec5d97..a60665f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ the semantic Type-to-Go mapping turns a class such as `User` into `*User`. - `val` and `var` - `Int`, `Long`, `String`, `Boolean`, `Unit` - function types like `(String) -> Unit` +- user generic functions and classes with inferred or explicit type arguments - `if`, `else`, `while`, and `for (item in items)` - function calls - lambdas like `{ x: Int -> println(x) }` and `{ println(it) }` @@ -126,6 +127,27 @@ checked during Gotlin compilation. A match used as an expression also requires every arm to return the same type. Block-style statement matches remain available for side effects. +## User generics + +Functions and classes may declare type parameters. Calls infer straightforward +type bindings from arguments or accept explicit type arguments: + +```kotlin +data class Box(var value: T) { + fun get(): T { return value } +} + +fun identity(value: T): T { return value } + +val number = identity(42) +val text = identity("value") +val box = Box("boxed") +``` + +Type parameters currently use an implicit `Any` constraint. Generic methods +with their own type parameters are intentionally deferred; place parameters on +the enclosing class or a top-level function. + ## Null safety Types are non-nullable by default. Add `?` explicitly when `null` is valid: diff --git a/cmd/gotlin-lsp/README.md b/cmd/gotlin-lsp/README.md index 50205f0..6aad845 100644 --- a/cmd/gotlin-lsp/README.md +++ b/cmd/gotlin-lsp/README.md @@ -13,7 +13,7 @@ Current support: - publish diagnostics from the existing Gotlin parser/code generator - `textDocument/hover` for packages, imports, and top-level functions - `textDocument/definition` for imported namespaces and top-level functions -- simple semantic diagnostics for duplicate imports/functions and undefined names +- structured resolver/type diagnostics from `lang.Analyze`, including package-context symbols - optional fallback to `gopls` for hover/definition on Go-imported symbols Build: diff --git a/cmd/gotlin-lsp/main.go b/cmd/gotlin-lsp/main.go index 507060e..790257d 100644 --- a/cmd/gotlin-lsp/main.go +++ b/cmd/gotlin-lsp/main.go @@ -160,6 +160,7 @@ type diagnostic struct { Range rng `json:"range"` Severity int `json:"severity,omitempty"` Source string `json:"source,omitempty"` + Code string `json:"code,omitempty"` Message string `json:"message"` } @@ -306,13 +307,13 @@ func buildDocumentState(text string) documentState { return state } state.program = program - if _, err := lang.Analyze(program); err != nil { - state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err)) - } + _, semanticDiagnostics := lang.AnalyzeWithContext(program, nil) state.symbols = indexSymbols(text, program) - state.diagnostics = semanticDiagnostics(text, program, state.symbols) - if _, err := lang.GenerateGo(program); err != nil { - state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err)) + state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics) + if len(semanticDiagnostics) == 0 { + if _, err := lang.GenerateGo(program); err != nil { + state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err)) + } } return state } @@ -325,10 +326,11 @@ func (s *server) buildDocumentState(uri, text string) documentState { return state } state.program = program - state.symbols = indexSymbols(text, program) programs, symbols := s.packageContext(uri, program.PackagePath, text) + _, semanticDiagnostics := lang.AnalyzeWithContext(program, programs) + state.symbols = indexSymbols(text, program) state.packageSymbols = symbols - state.diagnostics = semanticDiagnosticsWithPackage(text, program, state.symbols, programs) + state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics) return state } @@ -363,7 +365,9 @@ func (s *server) packageContext(currentURI, packagePath, currentText string) ([] if parseErr != nil || program.PackagePath != packagePath { return nil } - programs = append(programs, program) + if candidateURI != currentURI { + programs = append(programs, program) + } for _, sym := range indexSymbols(text, program) { sym.URI = candidateURI symbols = append(symbols, sym) @@ -729,227 +733,22 @@ func indexSymbols(text string, program *lang.Program) []symbol { return symbols } -func semanticDiagnostics(text string, program *lang.Program, symbols []symbol) []diagnostic { - return semanticDiagnosticsWithPackage(text, program, symbols, nil) -} - -func semanticDiagnosticsWithPackage(text string, program *lang.Program, symbols []symbol, packagePrograms []*lang.Program) []diagnostic { - var diagnostics []diagnostic - - funcSymbols := map[string]symbol{} - importSymbols := map[string]symbol{} - typeSymbols := map[string]symbol{} - for _, sym := range symbols { - switch sym.Kind { - case symbolKindFunction: - if prev, ok := funcSymbols[sym.Name]; ok { - diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate function "+sym.Name)) - diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate function "+sym.Name)) - } else { - funcSymbols[sym.Name] = sym - } - case symbolKindModule: - if prev, ok := importSymbols[sym.Name]; ok { - diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate import alias "+sym.Name)) - diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate import alias "+sym.Name)) - } else { - importSymbols[sym.Name] = sym - } - case symbolKindClass, symbolKindInterface, symbolKindEnum: - if prev, ok := typeSymbols[sym.Name]; ok { - diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate type "+sym.Name)) - diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate type "+sym.Name)) - } else { - typeSymbols[sym.Name] = sym - } +func diagnosticsFromSemantic(text string, semantic []lang.SemanticDiagnostic) []diagnostic { + diagnostics := make([]diagnostic, 0, len(semantic)) + for _, item := range semantic { + start := offsetToPosition(text, item.Span.Start) + endOffset := item.Span.End + if endOffset <= item.Span.Start { + endOffset = item.Span.Start + 1 } + diagnostics = append(diagnostics, diagnostic{ + Range: rng{Start: start, End: offsetToPosition(text, endOffset)}, + Severity: int(item.Severity), + Source: "gotlin", + Code: item.Code, + Message: item.Message, + }) } - - imports := map[string]bool{} - for _, imp := range program.Imports { - imports[importAlias(imp)] = true - } - functions := map[string]lang.FunctionDecl{} - for _, fn := range program.Functions { - functions[fn.Name] = fn - } - types := map[string]bool{} - for _, decl := range program.Interfaces { - types[decl.Name] = true - } - for _, decl := range program.Classes { - types[decl.Name] = true - } - for _, decl := range program.Enums { - types[decl.Name] = true - } - for _, sibling := range packagePrograms { - for _, fn := range sibling.Functions { - functions[fn.Name] = fn - } - for _, decl := range sibling.Interfaces { - types[decl.Name] = true - } - for _, decl := range sibling.Classes { - types[decl.Name] = true - } - for _, decl := range sibling.Enums { - types[decl.Name] = true - } - } - for _, fn := range program.Functions { - diagnostics = append(diagnostics, functionSemanticDiagnostics(text, fn, functions, imports, types, nil)...) - } - for _, class := range program.Classes { - fields := map[string]bool{ - "this": true, - } - for _, field := range class.Fields { - fields[field.Name] = true - } - for _, method := range class.Methods { - diagnostics = append(diagnostics, functionSemanticDiagnostics(text, method, functions, imports, types, fields)...) - } - } - - sort.SliceStable(diagnostics, func(i, j int) bool { - if diagnostics[i].Range.Start.Line != diagnostics[j].Range.Start.Line { - return diagnostics[i].Range.Start.Line < diagnostics[j].Range.Start.Line - } - return diagnostics[i].Range.Start.Character < diagnostics[j].Range.Start.Character - }) - return uniqueDiagnostics(diagnostics) -} - -func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions map[string]lang.FunctionDecl, imports map[string]bool, types map[string]bool, fields map[string]bool) []diagnostic { - scope := map[string]bool{} - for _, param := range fn.Params { - scope[param.Name] = true - } - for name := range fields { - scope[name] = true - } - var diagnostics []diagnostic - var walkExpr func(expr lang.Expr, scope map[string]bool) - var walkStmts func(stmts []lang.Stmt, scope map[string]bool) - - walkStmts = func(stmts []lang.Stmt, scope map[string]bool) { - for _, stmt := range stmts { - switch s := stmt.(type) { - case lang.VarDecl: - walkExpr(s.Value, scope) - scope[s.Name] = true - case lang.MultiVarDecl: - walkExpr(s.Value, scope) - for _, name := range s.Names { - scope[name] = true - } - case lang.AssignStmt: - if !scope[s.Name] { - diagnostics = append(diagnostics, undefinedNameDiagnostic(text, s.Name, "undefined variable "+s.Name)) - } - walkExpr(s.Value, scope) - case lang.AddAssignStmt: - if !scope[s.Name] { - diagnostics = append(diagnostics, undefinedNameDiagnostic(text, s.Name, "undefined variable "+s.Name)) - } - walkExpr(s.Value, scope) - case lang.MultiAssignStmt: - for _, name := range s.Names { - if !scope[name] { - diagnostics = append(diagnostics, undefinedNameDiagnostic(text, name, "undefined variable "+name)) - } - } - walkExpr(s.Value, scope) - case lang.ReturnStmt: - if s.Value != nil { - walkExpr(s.Value, scope) - } - case lang.ThrowStmt: - walkExpr(s.Value, scope) - case lang.DeferStmt: - walkExpr(s.Value, scope) - case lang.ExprStmt: - walkExpr(s.Value, scope) - case lang.IfStmt: - walkExpr(s.Cond, scope) - thenScope := copyScope(scope) - elseScope := copyScope(scope) - walkStmts(s.Then, thenScope) - walkStmts(s.Else, elseScope) - case lang.WhileStmt: - walkExpr(s.Cond, scope) - bodyScope := copyScope(scope) - walkStmts(s.Body, bodyScope) - case lang.ForEachStmt: - walkExpr(s.Source, scope) - bodyScope := copyScope(scope) - bodyScope[s.Name] = true - walkStmts(s.Body, bodyScope) - case lang.MatchStmt: - walkExpr(s.Value, scope) - for _, matchCase := range s.Cases { - caseScope := copyScope(scope) - for _, binding := range matchCase.Bindings { - caseScope[binding] = true - } - walkStmts(matchCase.Body, caseScope) - } - case lang.TryCatchStmt: - tryScope := copyScope(scope) - walkStmts(s.TryBody, tryScope) - catchScope := copyScope(scope) - catchScope[s.CatchName] = true - walkStmts(s.CatchBody, catchScope) - } - } - } - - walkExpr = func(expr lang.Expr, scope map[string]bool) { - switch e := expr.(type) { - case lang.IdentExpr: - if isBuiltin(e.Name) || scope[e.Name] || functions[e.Name].Name != "" || imports[e.Name] || types[e.Name] { - return - } - diagnostics = append(diagnostics, undefinedNameDiagnostic(text, e.Name, "undefined identifier "+e.Name)) - case lang.UnaryExpr: - walkExpr(e.Value, scope) - case lang.BinaryExpr: - walkExpr(e.Left, scope) - walkExpr(e.Right, scope) - case lang.CallExpr: - walkExpr(e.Callee, scope) - for _, arg := range e.Args { - walkExpr(arg, scope) - } - case lang.SelectorExpr: - walkExpr(e.Receiver, scope) - case lang.SafeSelectorExpr: - walkExpr(e.Receiver, scope) - case lang.NonNullExpr: - walkExpr(e.Value, scope) - case lang.IndexExpr: - walkExpr(e.Receiver, scope) - walkExpr(e.Index, scope) - case lang.EnumVariantExpr: - for _, value := range e.Values { - walkExpr(value, scope) - } - case lang.LambdaExpr: - lambdaScope := copyScope(scope) - if e.ImplicitIt { - lambdaScope["it"] = true - } - for _, param := range e.Params { - lambdaScope[param.Name] = true - } - walkStmts(e.Body, lambdaScope) - case lang.NullExpr: - return - } - } - - walkStmts(fn.Body, scope) return diagnostics } @@ -973,25 +772,6 @@ func diagnosticFromError(text string, err error) diagnostic { } } -func duplicateDiagnostic(r rng, message string) diagnostic { - return diagnostic{ - Range: r, - Severity: diagnosticSeverityWarning, - Source: "gotlin", - Message: message, - } -} - -func undefinedNameDiagnostic(text string, name string, message string) diagnostic { - r := findWordRange(text, name) - return diagnostic{ - Range: r, - Severity: diagnosticSeverityWarning, - Source: "gotlin", - Message: message, - } -} - func offsetToPosition(text string, offset int) position { runes := []rune(text) if offset < 0 { @@ -1221,7 +1001,11 @@ func renderImportDetail(imp lang.ImportDecl) string { } func renderFunctionSignature(fn lang.FunctionDecl) string { - return renderFunctionSignatureFromParts(fn.Name, fn.Params, fn.ReturnType) + name := fn.Name + if len(fn.TypeParams) > 0 { + name += "<" + strings.Join(fn.TypeParams, ", ") + ">" + } + return renderFunctionSignatureFromParts(name, fn.Params, fn.ReturnType) } func renderFunctionSignatureFromParts(name string, fnParams []lang.Param, returnType string) string { @@ -1244,6 +1028,11 @@ func renderClassSignature(decl lang.ClassDecl) string { var b strings.Builder b.WriteString("class ") b.WriteString(decl.Name) + if len(decl.TypeParams) > 0 { + b.WriteString("<") + b.WriteString(strings.Join(decl.TypeParams, ", ")) + b.WriteString(">") + } var fields []string for _, field := range decl.Fields { @@ -1289,289 +1078,59 @@ func renderVariableSignature(name string, mutable bool, typ string) string { } func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo { - functions := map[string]lang.FunctionDecl{} - for _, fn := range program.Functions { - functions[fn.Name] = fn - } - classes := map[string]lang.ClassDecl{} - for _, class := range program.Classes { - classes[class.Name] = class - } - - var out []variableDeclInfo - var walkStmts func(stmts []lang.Stmt, scope map[string]string) - var inferExprType func(expr lang.Expr, scope map[string]string) string - var inferCallReturnTypes func(call lang.CallExpr, scope map[string]string) []string - - inferCallReturnTypes = func(call lang.CallExpr, scope map[string]string) []string { - switch callee := call.Callee.(type) { - case lang.IdentExpr: - if fn, ok := functions[callee.Name]; ok { - return []string{fn.ReturnType} - } - if _, ok := classes[callee.Name]; ok { - return []string{callee.Name} - } - switch callee.Name { - case "println": - return []string{"Unit"} - case "Channel": - if len(call.TypeArgs) == 1 { - return []string{"Channel<" + call.TypeArgs[0] + ">"} - } - return []string{"Channel"} - case "listOf", "mutableListOf": - if len(call.TypeArgs) == 1 { - return []string{"List<" + call.TypeArgs[0] + ">"} - } - if len(call.Args) > 0 { - elemType := inferExprType(call.Args[0], scope) - for i := 1; i < len(call.Args); i++ { - argType := inferExprType(call.Args[i], scope) - if argType == "" || elemType == "" || argType != elemType { - elemType = "" - break - } - } - if elemType != "" { - return []string{"List<" + elemType + ">"} - } - } - return []string{"List"} - case "mapOf", "mutableMapOf": - if len(call.TypeArgs) == 2 { - return []string{"Map<" + call.TypeArgs[0] + ", " + call.TypeArgs[1] + ">"} - } - if len(call.Args) > 0 && len(call.Args)%2 == 0 { - keyType := inferExprType(call.Args[0], scope) - valType := inferExprType(call.Args[1], scope) - for i := 2; i < len(call.Args); i += 2 { - nextKeyType := inferExprType(call.Args[i], scope) - nextValType := inferExprType(call.Args[i+1], scope) - if keyType == "" || nextKeyType == "" || keyType != nextKeyType { - keyType = "" - } - if valType == "" || nextValType == "" || valType != nextValType { - valType = "" - } - } - if keyType != "" && valType != "" { - return []string{"Map<" + keyType + ", " + valType + ">"} - } - } - return []string{"Map"} - default: - return nil - } - case lang.SelectorExpr: - receiverType := inferExprType(callee.Receiver, scope) - if callee.Name == "read" { - if elemType, ok := channelElementType(receiverType); ok { - return []string{elemType} - } - return nil - } - if callee.Name == "send" { - return []string{"Unit"} - } - switch receiverType { - case "*sql.DB", "sql.DB": - switch callee.Name { - case "Ping": - return []string{"error"} - } - case "*bun.DB", "bun.DB": - switch callee.Name { - case "NewSelect": - return []string{"*bun.SelectQuery"} - case "NewCreateTable": - return []string{"*bun.CreateTableQuery"} - case "NewInsert": - return []string{"*bun.InsertQuery"} - } - case "*bun.SelectQuery", "bun.SelectQuery": - switch callee.Name { - case "ColumnExpr", "Model": - return []string{"*bun.SelectQuery"} - case "Scan": - return []string{"error"} - case "Count": - return []string{"Int", "error"} - } - case "*bun.CreateTableQuery", "bun.CreateTableQuery": - switch callee.Name { - case "Model", "IfNotExists": - return []string{"*bun.CreateTableQuery"} - case "Exec": - return []string{"sql.Result", "error"} - } - case "*bun.InsertQuery", "bun.InsertQuery": - switch callee.Name { - case "Model": - return []string{"*bun.InsertQuery"} - case "Exec": - return []string{"sql.Result", "error"} - } - } - if path, ok := selectorPathLang(call.Callee); ok { - switch path { - case "context.Background": - return []string{"context.Context"} - case "sql.OpenDB": - return []string{"*sql.DB"} - case "bun.NewDB": - return []string{"*bun.DB"} - case "pgdriver.NewConnector": - return []string{"pgdriver.Connector"} - } - } - } - return nil - } - - inferExprType = func(expr lang.Expr, scope map[string]string) string { - switch e := expr.(type) { - case lang.IntExpr: - return "Int" - case lang.StringExpr: - return "String" - case lang.BoolExpr: - return "Boolean" - case lang.NullExpr: - return "null" - case lang.IdentExpr: - return scope[e.Name] - case lang.CallExpr: - types := inferCallReturnTypes(e, scope) - if len(types) > 0 { - return types[0] - } - return "" - case lang.SelectorExpr: - receiverType := inferExprType(e.Receiver, scope) - if receiverType == "" { - return "" - } - trimmed := strings.TrimPrefix(receiverType, "*") - if class, ok := classes[trimmed]; ok { - for _, field := range class.Fields { - if field.Name == e.Name { - return field.Type - } - } - } - return "" - default: - return "" - } - } - - walkStmts = func(stmts []lang.Stmt, scope map[string]string) { - for _, stmt := range stmts { - switch s := stmt.(type) { + var result []variableDeclInfo + var walk func([]lang.Stmt) + walk = func(statements []lang.Stmt) { + for _, statement := range statements { + switch value := statement.(type) { case lang.VarDecl: - typ := s.Type + typ := value.Type if typ == "" { - resolved := lang.ResolvedType(s.Value) + resolved := lang.ResolvedType(value.Value) if resolved.String() != "" { typ = resolved.String() - } else { - typ = inferExprType(s.Value, scope) } } - out = append(out, variableDeclInfo{Name: s.Name, Mutable: s.Mutable, Type: typ}) - scope[s.Name] = typ + result = append(result, variableDeclInfo{Name: value.Name, Mutable: value.Mutable, Type: typ}) case lang.MultiVarDecl: - types := []string(nil) - if call, ok := s.Value.(lang.CallExpr); ok { - types = inferCallReturnTypes(call, scope) - } - for i, name := range s.Names { - typ := "" - if i < len(types) { - typ = types[i] + types := make([]string, len(value.Names)) + if fallible, ok := lang.ResolvedType(value.Value).(lang.GenericType); ok && fallible.Base.String() == "Result" && len(fallible.Args) == 2 && len(types) == 2 { + types[0] = fallible.Args[0].String() + types[1] = fallible.Args[1].String() + "?" + } else if tuple, ok := lang.ResolvedType(value.Value).(lang.TupleType); ok && len(tuple.Elements) == len(types) { + for index, element := range tuple.Elements { + types[index] = element.String() } - out = append(out, variableDeclInfo{Name: name, Mutable: s.Mutable, Type: typ}) - scope[name] = typ } - case lang.AddAssignStmt: - if _, ok := scope[s.Name]; !ok { - scope[s.Name] = "" + for index, name := range value.Names { + result = append(result, variableDeclInfo{Name: name, Mutable: value.Mutable, Type: types[index]}) } case lang.IfStmt: - thenScope := copyTypeScope(scope) - elseScope := copyTypeScope(scope) - walkStmts(s.Then, thenScope) - walkStmts(s.Else, elseScope) + walk(value.Then) + walk(value.Else) case lang.WhileStmt: - bodyScope := copyTypeScope(scope) - walkStmts(s.Body, bodyScope) + walk(value.Body) + case lang.ForEachStmt: + walk(value.Body) + case lang.MatchStmt: + for _, matchCase := range value.Cases { + walk(matchCase.Body) + } case lang.TryCatchStmt: - tryScope := copyTypeScope(scope) - walkStmts(s.TryBody, tryScope) - catchScope := copyTypeScope(scope) - catchScope[s.CatchName] = s.CatchType - walkStmts(s.CatchBody, catchScope) + walk(value.TryBody) + walk(value.CatchBody) } } } - - for _, fn := range program.Functions { - scope := map[string]string{} - for _, param := range fn.Params { - scope[param.Name] = param.Type - } - walkStmts(fn.Body, scope) + for _, function := range program.Functions { + walk(function.Body) } for _, class := range program.Classes { - fieldScope := map[string]string{} - for _, field := range class.Fields { - fieldScope[field.Name] = field.Type - } for _, method := range class.Methods { - scope := copyTypeScope(fieldScope) - for _, param := range method.Params { - scope[param.Name] = param.Type - } - walkStmts(method.Body, scope) + walk(method.Body) } } - return out -} - -func copyTypeScope(scope map[string]string) map[string]string { - dup := make(map[string]string, len(scope)) - for k, v := range scope { - dup[k] = v - } - return dup -} - -func channelElementType(typ string) (string, bool) { - typ = strings.TrimSpace(typ) - if !strings.HasPrefix(typ, "Channel<") || !strings.HasSuffix(typ, ">") { - return "", false - } - inner := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(typ, "Channel<"), ">")) - if inner == "" { - return "", false - } - return inner, true -} - -func selectorPathLang(expr lang.Expr) (string, bool) { - switch e := expr.(type) { - case lang.IdentExpr: - return e.Name, true - case lang.SelectorExpr: - left, ok := selectorPathLang(e.Receiver) - if !ok { - return "", false - } - return left + "." + e.Name, true - default: - return "", false - } + return result } func resolveStdlibTarget(state documentState, pos position) (stdlibTarget, bool) { diff --git a/cmd/gotlin-lsp/main_test.go b/cmd/gotlin-lsp/main_test.go index 9101674..6508fe0 100644 --- a/cmd/gotlin-lsp/main_test.go +++ b/cmd/gotlin-lsp/main_test.go @@ -450,6 +450,21 @@ fun main() { } } +func TestBuildDocumentStateUsesStructuredSemanticDiagnostics(t *testing.T) { + text := "package demo\nfun main() { println(missing) }" + state := buildDocumentState(text) + if len(state.diagnostics) != 1 { + t.Fatalf("diagnostics = %#v", state.diagnostics) + } + diagnostic := state.diagnostics[0] + if diagnostic.Code != "undefined-symbol" || diagnostic.Severity != diagnosticSeverityError || diagnostic.Message != "undefined identifier missing" { + t.Fatalf("unexpected diagnostic: %#v", diagnostic) + } + if diagnostic.Range.Start.Line != 1 || diagnostic.Range.Start.Character != 21 { + t.Fatalf("unexpected diagnostic range: %#v", diagnostic.Range) + } +} + func TestBuildDocumentStateInfersHttpServerVariableTypes(t *testing.T) { text := strings.TrimSpace(` package demo @@ -460,7 +475,7 @@ import github.com.uptrace.bun class C(val db: *bun.DB) { fun run() { val ctx = context.Background() - val total = db.NewSelect().ColumnExpr("1").Count(ctx) + val total = db.NewSelect().ColumnExpr("1").Count(ctx).unwrap() println(total) } } diff --git a/examples/generics.gt b/examples/generics.gt new file mode 100644 index 0000000..c6be404 --- /dev/null +++ b/examples/generics.gt @@ -0,0 +1,15 @@ +package main + +data class Box(var value: T) { + fun get(): T { return value } +} + +fun identity(value: T): T { return value } + +fun main() { + val number = identity(42) + val text = identity("value") + val box = Box(text) + println(number) + println(box.get()) +} diff --git a/examples/http_server.gt b/examples/http_server.gt index c7a11bf..bc80a23 100644 --- a/examples/http_server.gt +++ b/examples/http_server.gt @@ -30,7 +30,7 @@ class EpicControllerImpl(val db: *bun.DB) { db.NewCreateTable().Model(model).IfNotExists().Exec(ctx) val user = BunUser("user-from-gotlin") db.NewInsert().Model(user).Exec(ctx) - val total = db.NewSelect().Model(model).Count(ctx) + val total = db.NewSelect().Model(model).Count(ctx).unwrap() fmt.Fprintln(w, "bun users total:", total) } diff --git a/go.mod b/go.mod index b9f127a..05d51e2 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module gotlin go 1.25.6 -require github.com/jackc/pgx/v5 v5.7.6 +require ( + github.com/jackc/pgx/v5 v5.7.6 + golang.org/x/tools v0.49.0 +) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -21,8 +24,9 @@ require ( go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/mod v0.39.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.34.0 // indirect mellium.im/sasl v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index 007e8ab..567fa3f 100644 --- a/go.sum +++ b/go.sum @@ -37,12 +37,19 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= diff --git a/internal/lang/ast.go b/internal/lang/ast.go index ebe8c63..f8c7a5b 100644 --- a/internal/lang/ast.go +++ b/internal/lang/ast.go @@ -10,7 +10,10 @@ type Program struct { Embeds []EmbedDecl } -type EmbedDecl struct{ Path, Name, Type string } +type EmbedDecl struct { + Path, Name, Type string + TypeRef TypeRef +} type EnumDecl struct { Name string @@ -20,6 +23,7 @@ type EnumDecl struct { type EnumVariant struct { Name string PayloadTypes []string + PayloadRefs []TypeRef StringValue string } @@ -35,6 +39,7 @@ type InterfaceDecl struct { type ClassDecl struct { Name string + TypeParams []string Data bool JSONNaming string Table string @@ -48,6 +53,7 @@ type FieldDecl struct { Private bool Name string Type string + TypeRef TypeRef Column string ID bool Generated bool @@ -55,22 +61,27 @@ type FieldDecl struct { type FunctionSignature struct { Name string + TypeParams []string Params []Param ReturnType string + ReturnRef TypeRef Suspend bool } type FunctionDecl struct { Name string + TypeParams []string Params []Param ReturnType string + ReturnRef TypeRef Body []Stmt Suspend bool } type Param struct { - Name string - Type string + Name string + Type string + TypeRef TypeRef } type Stmt interface { @@ -84,7 +95,9 @@ type Expr interface { type VarDecl struct { Mutable bool Name string + Pos int Type string + TypeRef TypeRef Value Expr } @@ -173,6 +186,7 @@ type TryCatchStmt struct { TryBody []Stmt CatchName string CatchType string + CatchRef TypeRef CatchBody []Stmt } @@ -208,6 +222,7 @@ type MatchExprCase struct { type IdentExpr struct { Meta ExprMeta Name string + Pos int } func (IdentExpr) exprNode() {} @@ -280,6 +295,7 @@ type SelectorExpr struct { Meta ExprMeta Receiver Expr Name string + NamePos int } func (SelectorExpr) exprNode() {} @@ -288,6 +304,7 @@ type SafeSelectorExpr struct { Meta ExprMeta Receiver Expr Name string + NamePos int } func (SafeSelectorExpr) exprNode() {} diff --git a/internal/lang/diagnostics.go b/internal/lang/diagnostics.go new file mode 100644 index 0000000..1abd68a --- /dev/null +++ b/internal/lang/diagnostics.go @@ -0,0 +1,50 @@ +package lang + +import ( + "fmt" + "regexp" + "strconv" +) + +type DiagnosticSeverity int + +const ( + DiagnosticError DiagnosticSeverity = 1 + DiagnosticWarning DiagnosticSeverity = 2 +) + +type SourceSpan struct { + Start int + End int +} + +type SemanticDiagnostic struct { + Code string + Message string + Severity DiagnosticSeverity + Span SourceSpan +} + +func (diagnostic SemanticDiagnostic) Error() string { return diagnostic.Message } + +var semanticOffsetPattern = regexp.MustCompile(`\sat\s(\d+)$`) + +func diagnosticForError(code string, err error) SemanticDiagnostic { + span := SourceSpan{} + if match := semanticOffsetPattern.FindStringSubmatch(err.Error()); len(match) == 2 { + if offset, parseErr := strconv.Atoi(match[1]); parseErr == nil { + span = SourceSpan{Start: offset, End: offset + 1} + } + } + return SemanticDiagnostic{Code: code, Message: err.Error(), Severity: DiagnosticError, Span: span} +} + +func undefinedDiagnostic(name string, position int) SemanticDiagnostic { + end := position + len(name) + return SemanticDiagnostic{ + Code: "undefined-symbol", + Message: fmt.Sprintf("undefined identifier %s", name), + Severity: DiagnosticError, + Span: SourceSpan{Start: position, End: end}, + } +} diff --git a/internal/lang/diagnostics_test.go b/internal/lang/diagnostics_test.go new file mode 100644 index 0000000..19b9f3f --- /dev/null +++ b/internal/lang/diagnostics_test.go @@ -0,0 +1,37 @@ +package lang + +import "testing" + +func TestAnalyzeProducesStructuredUndefinedDiagnostic(t *testing.T) { + source := `package demo fun main() { println(missing) }` + program, err := Parse(source) + if err != nil { + t.Fatal(err) + } + _, diagnostics := AnalyzeWithContext(program, nil) + if len(diagnostics) != 1 { + t.Fatalf("diagnostics = %#v", diagnostics) + } + diagnostic := diagnostics[0] + if diagnostic.Code != "undefined-symbol" || diagnostic.Severity != DiagnosticError || diagnostic.Message != "undefined identifier missing" { + t.Fatalf("unexpected diagnostic: %#v", diagnostic) + } + if got := source[diagnostic.Span.Start:diagnostic.Span.End]; got != "missing" { + t.Fatalf("diagnostic span = %q", got) + } +} + +func TestAnalyzeUsesAdditionalPackagePrograms(t *testing.T) { + current, err := Parse(`package demo fun main() { helper() }`) + if err != nil { + t.Fatal(err) + } + sibling, err := Parse(`package demo fun helper() { println("ok") }`) + if err != nil { + t.Fatal(err) + } + _, diagnostics := AnalyzeWithContext(current, []*Program{sibling}) + if len(diagnostics) != 0 { + t.Fatalf("package symbol was not resolved: %#v", diagnostics) + } +} diff --git a/internal/lang/generate_go.go b/internal/lang/generate_go.go index 311b460..16cc9aa 100644 --- a/internal/lang/generate_go.go +++ b/internal/lang/generate_go.go @@ -263,6 +263,7 @@ func (g *goGenerator) function(fn FunctionDecl) error { } g.write("func ") g.write(fn.Name) + g.write(renderGoTypeParameters(fn.TypeParams)) g.write(g.renderGoFunctionParams(fn)) if ret := g.goReturnType(fn.ReturnType); ret != "" { g.write(" ") @@ -346,7 +347,7 @@ func enumVariant(decl EnumDecl, name string) *EnumVariant { } func (g *goGenerator) classDecl(class ClassDecl) error { - g.line("type " + class.Name + " struct {") + g.line("type " + class.Name + renderGoTypeParameters(class.TypeParams) + " struct {") g.indentLevel++ for _, field := range class.Fields { name := field.Name @@ -366,6 +367,7 @@ func (g *goGenerator) classDecl(class ClassDecl) error { g.line("") g.write("func New") g.write(class.Name) + g.write(renderGoTypeParameters(class.TypeParams)) g.write("(") for i, field := range class.Fields { if i > 0 { @@ -377,11 +379,13 @@ func (g *goGenerator) classDecl(class ClassDecl) error { } g.write(") *") g.write(class.Name) + g.write(renderGoTypeArguments(class.TypeParams)) g.write(" {\n") g.indentLevel++ g.writeIndent() g.write("return &") g.write(class.Name) + g.write(renderGoTypeArguments(class.TypeParams)) g.write("{") for i, field := range class.Fields { if i > 0 { @@ -427,6 +431,7 @@ func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error { g.write("func (self *") g.write(class.Name) + g.write(renderGoTypeArguments(class.TypeParams)) g.write(") ") g.write(fn.Name) g.write(g.renderGoFunctionParams(fn)) @@ -792,8 +797,8 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { if e.Name == "this" { return "self", nil } - if g.classField(*g.currentClass, e.Name) && !g.isDefined(e.Name) { - return "self." + e.Name, nil + if field, found := classFieldByName(*g.currentClass, e.Name); found && !g.isDefined(e.Name) { + return "self." + mappingFieldName(*g.currentClass, field), nil } } return e.Name, nil @@ -1189,7 +1194,7 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } if ident, ok := e.Callee.(IdentExpr); ok { if _, ok := g.semantic.Classes[ident.Name]; ok && (resolvedCall == nil || resolvedCall.Meaning == ClassConstructionExpr) { - return fmt.Sprintf("New%s(%s)", ident.Name, strings.Join(args, ", ")), nil + return fmt.Sprintf("New%s%s(%s)", ident.Name, g.renderCallTypeArguments(e.TypeArgs), strings.Join(args, ", ")), nil } } if ident, ok := e.Callee.(IdentExpr); ok { @@ -1217,6 +1222,9 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { if err != nil { return "", err } + if len(e.TypeArgs) > 0 { + callee += g.renderCallTypeArguments(e.TypeArgs) + } call := fmt.Sprintf("%s(%s)", callee, strings.Join(args, ", ")) if result, ok := g.goErrorResult(e, call, expectedType); ok { return result, nil @@ -2460,18 +2468,11 @@ func (g *goGenerator) isDefined(name string) bool { return false } -func (g *goGenerator) classField(class ClassDecl, name string) bool { - for _, field := range class.Fields { - if field.Name == name { - return true - } - } - return false -} - func (g *goGenerator) assignTarget(name string) string { - if g.currentClass != nil && g.classField(*g.currentClass, name) && !g.isDefined(name) { - return "self." + name + if g.currentClass != nil && !g.isDefined(name) { + if field, found := classFieldByName(*g.currentClass, name); found { + return "self." + mappingFieldName(*g.currentClass, field) + } } return name } @@ -2496,6 +2497,35 @@ func (g *goGenerator) renderGoFunctionParams(function FunctionDecl) string { return g.renderGoParamsWithPrefix(function.Params, prefix) } +func renderGoTypeParameters(params []string) string { + if len(params) == 0 { + return "" + } + values := make([]string, len(params)) + for index, param := range params { + values[index] = param + " any" + } + return "[" + strings.Join(values, ", ") + "]" +} + +func renderGoTypeArguments(params []string) string { + if len(params) == 0 { + return "" + } + return "[" + strings.Join(params, ", ") + "]" +} + +func (g *goGenerator) renderCallTypeArguments(args []string) string { + if len(args) == 0 { + return "" + } + values := make([]string, len(args)) + for index, argument := range args { + values[index] = g.goType(argument) + } + return "[" + strings.Join(values, ", ") + "]" +} + func (g *goGenerator) renderGoParamsWithPrefix(params []Param, prefix string) string { var b strings.Builder b.WriteString("(") diff --git a/internal/lang/generics_test.go b/internal/lang/generics_test.go new file mode 100644 index 0000000..e8603e0 --- /dev/null +++ b/internal/lang/generics_test.go @@ -0,0 +1,72 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestGenerateGenericFunctionsAndClasses(t *testing.T) { + program, err := Parse(`package demo +data class Box(var value: T) { + fun get(): T { return value } +} +fun identity(value: T): T { return value } +fun wrap(value: T): Box { return Box(value) } +fun main() { + val inferred = identity(42) + val explicit = identity("value") + val box = wrap("boxed") + println(box.get()) +}`) + if err != nil { + t.Fatal(err) + } + output, err := GenerateGo(program) + if err != nil { + t.Fatal(err) + } + code := string(output) + for _, expected := range []string{ + "type Box[T any] struct", + "func NewBox[T any](value T) *Box[T]", + "func (self *Box[T]) get() T", + "func identity[T any](value T) T", + "func wrap[T any](value T) *Box[T]", + "identity(42)", + `identity[string]("value")`, + `wrap("boxed")`, + } { + if !strings.Contains(code, expected) { + t.Fatalf("missing %q:\n%s", expected, code) + } + } + main := program.Functions[2] + inferred := main.Body[0].(VarDecl) + if got := ResolvedType(inferred.Value).String(); got != "Int" { + t.Fatalf("inferred generic type = %s", got) + } + box := main.Body[2].(VarDecl) + if got := ResolvedType(box.Value).String(); got != "Box" { + t.Fatalf("generic class type = %s", got) + } +} + +func TestRejectInvalidGenericDeclarationsAndCalls(t *testing.T) { + for _, test := range []struct{ source, message string }{ + {`package demo fun identity(value: T): T { return value }`, "duplicate type parameter T"}, + {`package demo fun identity(value: T): T { return value } fun main() { identity("x") }`, "expects 1 type arguments, got 2"}, + {`package demo class Box(val value: T) { fun convert(): T { return value } }`, "generic methods are not supported"}, + } { + program, err := Parse(test.source) + if err != nil { + if strings.Contains(err.Error(), test.message) { + continue + } + t.Fatalf("unexpected parse error: %v", err) + } + _, err = GenerateGo(program) + if err == nil || !strings.Contains(err.Error(), test.message) { + t.Fatalf("error = %v, want %q", err, test.message) + } + } +} diff --git a/internal/lang/go_interop_test.go b/internal/lang/go_interop_test.go index ba8a548..fc81c0d 100644 --- a/internal/lang/go_interop_test.go +++ b/internal/lang/go_interop_test.go @@ -12,7 +12,7 @@ package demo import http net.http import json encoding.json -fun main() { +fun main(handler: (http.ResponseWriter, *http.Request) -> Unit, body: ByteSlice, target: Any) { http.handleFunc("/health", handler) json.unmarshal(body, &target) } diff --git a/internal/lang/go_types.go b/internal/lang/go_types.go index 202c0bb..9a14c37 100644 --- a/internal/lang/go_types.go +++ b/internal/lang/go_types.go @@ -4,14 +4,32 @@ import ( "go/importer" gotypes "go/types" "strings" + "sync" + + golangpackages "golang.org/x/tools/go/packages" ) +var importedGoPackages sync.Map + func importGoPackage(decl ImportDecl) (*gotypes.Package, error) { path := strings.Trim(decl.Path, `"`) if !strings.Contains(path, "/") { path = importPathToGoPath(path) } - return importer.Default().Import(path) + if cached, ok := importedGoPackages.Load(path); ok { + return cached.(*gotypes.Package), nil + } + pkg, err := importer.Default().Import(path) + if err == nil { + importedGoPackages.Store(path, pkg) + return pkg, nil + } + loaded, loadErr := golangpackages.Load(&golangpackages.Config{Mode: golangpackages.NeedName | golangpackages.NeedTypes | golangpackages.NeedImports | golangpackages.NeedDeps}, path) + if loadErr != nil || len(loaded) == 0 || loaded[0].Types == nil { + return nil, err + } + importedGoPackages.Store(path, loaded[0].Types) + return loaded[0].Types, nil } func (semantic *SemanticProgram) goSelectorType(alias, name string) Type { @@ -119,7 +137,11 @@ func semanticTypeFromGoResults(results *gotypes.Tuple) Type { if results.Len() == 1 { return last } - return UnknownType{} + elements := make([]Type, results.Len()) + for index := range elements { + elements[index] = semanticTypeFromGo(results.At(index).Type()) + } + return TupleType{Elements: elements} } func semanticTypeFromGo(typ gotypes.Type) Type { diff --git a/internal/lang/go_types_test.go b/internal/lang/go_types_test.go new file mode 100644 index 0000000..2d5a19c --- /dev/null +++ b/internal/lang/go_types_test.go @@ -0,0 +1,32 @@ +package lang + +import "testing" + +func TestGoTypesResolvesFallibleCallsMethodsAndTuples(t *testing.T) { + program, err := Parse(`package demo +import os +import strconv +fun main() { + val parsed = strconv.atoi("42") + val value, found = os.lookupEnv("HOME") + println(parsed) + println(value) + println(found) +}`) + if err != nil { + t.Fatal(err) + } + _, diagnostics := AnalyzeWithContext(program, nil) + if len(diagnostics) != 0 { + t.Fatalf("diagnostics: %#v", diagnostics) + } + body := program.Functions[0].Body + parsed := body[0].(VarDecl) + if got := ResolvedType(parsed.Value).String(); got != "Result" { + t.Fatalf("Atoi type = %s", got) + } + lookup := body[1].(MultiVarDecl) + if got := ResolvedType(lookup.Value).String(); got != "(String, Boolean)" { + t.Fatalf("LookupEnv type = %s", got) + } +} diff --git a/internal/lang/hir.go b/internal/lang/hir.go index 5cbfe53..245df71 100644 --- a/internal/lang/hir.go +++ b/internal/lang/hir.go @@ -28,6 +28,7 @@ const ( type ExprMeta struct{ Semantic *HIRExpr } type HIRExpr struct { + ID int Type Type Meaning ExprMeaning Symbol *Symbol @@ -111,8 +112,9 @@ type HIRFunction struct { } type HIRProgram struct { - Functions []*HIRFunction - Methods []*HIRFunction + Functions []*HIRFunction + Methods []*HIRFunction + Expressions []*HIRExpr } func exprMeta(expr Expr) *HIRExpr { @@ -240,10 +242,10 @@ func (resolver *semanticResolver) resolve() error { symbol, _ := resolver.program.Global.Lookup(decl.Name) scope := NewScope(resolver.program.Global) for _, param := range decl.Params { - typ, _ := resolver.program.ResolveType(param.Type) + typ, _ := resolver.program.ResolveTypeRefWithParams(param.TypeRef, decl.TypeParams) _ = scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ}) } - result, _ := resolver.program.ResolveType(decl.ReturnType) + result, _ := resolver.program.ResolveTypeRefWithParams(decl.ReturnRef, decl.TypeParams) resolver.resolveStmts(decl.Body, scope, nil, result) resolver.program.HIR.Functions = append(resolver.program.HIR.Functions, &HIRFunction{Symbol: symbol, Decl: decl, Scope: scope}) } @@ -255,10 +257,12 @@ func (resolver *semanticResolver) resolve() error { scope := NewScope(resolver.program.Global) _ = scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}}) for _, param := range method.Params { - typ, _ := resolver.program.ResolveType(param.Type) + params := append(append([]string{}, decl.TypeParams...), method.TypeParams...) + typ, _ := resolver.program.ResolveTypeRefWithParams(param.TypeRef, params) _ = scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ}) } - result, _ := resolver.program.ResolveType(method.ReturnType) + params := append(append([]string{}, decl.TypeParams...), method.TypeParams...) + result, _ := resolver.program.ResolveTypeRefWithParams(method.ReturnRef, params) resolver.resolveStmts(method.Body, scope, class, result) resolver.program.HIR.Methods = append(resolver.program.HIR.Methods, &HIRFunction{Symbol: class.Methods[method.Name], Decl: method, Scope: scope}) } @@ -289,6 +293,8 @@ func (resolver *semanticResolver) resolveStmts(stmts []Stmt, scope *Scope, class if result, ok := ResolvedType(value.Value).(GenericType); ok && result.Base.String() == "Result" && len(result.Args) == 2 && len(valueTypes) == 2 { valueTypes[0] = result.Args[0] valueTypes[1] = NullableType{Element: result.Args[1]} + } else if tuple, ok := ResolvedType(value.Value).(TupleType); ok && len(tuple.Elements) == len(valueTypes) { + copy(valueTypes, tuple.Elements) } for index, name := range value.Names { _ = scope.Define(&Symbol{Name: name, Kind: VariableSymbol, Type: valueTypes[index], Mutable: value.Mutable}) @@ -298,13 +304,34 @@ func (resolver *semanticResolver) resolveStmts(stmts []Stmt, scope *Scope, class expected := Type(UnknownType{}) if symbol, ok := scope.Lookup(value.Name); ok { expected = symbol.Type + } else if class != nil && class.Fields[value.Name] != nil { + expected = class.Fields[value.Name].Type + } else { + resolver.addDiagnostic(SemanticDiagnostic{Code: "undefined-variable", Message: "undefined variable " + value.Name, Severity: DiagnosticError, Span: SourceSpan{Start: value.Pos, End: value.Pos + len(value.Name)}}) } value.Value, _ = resolver.resolveExpr(value.Value, scope, class, expected) stmts[index] = value case AddAssignStmt: + _, local := scope.Lookup(value.Name) + field := false + if class != nil { + _, field = class.Fields[value.Name] + } + if !local && !field { + resolver.addDiagnostic(SemanticDiagnostic{Code: "undefined-variable", Message: "undefined variable " + value.Name, Severity: DiagnosticError, Span: SourceSpan{Start: value.Pos, End: value.Pos + len(value.Name)}}) + } value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{}) stmts[index] = value case MultiAssignStmt: + for index, name := range value.Names { + if _, ok := scope.Lookup(name); !ok { + position := 0 + if index < len(value.Positions) { + position = value.Positions[index] + } + resolver.addDiagnostic(SemanticDiagnostic{Code: "undefined-variable", Message: "undefined variable " + name, Severity: DiagnosticError, Span: SourceSpan{Start: position, End: position + len(name)}}) + } + } value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{}) stmts[index] = value case ReturnStmt: @@ -421,8 +448,11 @@ func (resolver *semanticResolver) resolveExpr(expr Expr, scope *Scope, class *Cl expr = value case LambdaExpr: lambdaScope := NewScope(scope) + if value.ImplicitIt { + _ = lambdaScope.Define(&Symbol{Name: "it", Kind: VariableSymbol, Type: UnknownType{}}) + } for _, param := range value.Params { - typ, _ := resolver.program.ResolveType(param.Type) + typ, _ := resolver.program.ResolveTypeRef(param.TypeRef) _ = lambdaScope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ}) } resolver.resolveStmts(value.Body, lambdaScope, class, functionResult(expected)) @@ -443,15 +473,20 @@ func (resolver *semanticResolver) resolveExpr(expr Expr, scope *Scope, class *Cl } } meaning, symbol := resolver.meaning(expr, scope) + if ident, ok := expr.(IdentExpr); ok && meaning == UnresolvedExpr && isUnknownType(typ) && !isSemanticBuiltin(ident.Name) { + resolver.addDiagnostic(undefinedDiagnostic(ident.Name, ident.Pos)) + } resolver.validateEnumExpression(expr, meaning) + resolver.validateGenericCall(expr) if meaning == GoCallExpr && isResultType(expected) { typ = expected } if nullable, ok := typ.(NullableType); ok && typeEqual(nullable.Element, expected) { typ = expected } - semantic := &HIRExpr{Type: typ, Meaning: meaning, Symbol: symbol} + semantic := &HIRExpr{ID: len(resolver.program.HIR.Expressions) + 1, Type: typ, Meaning: meaning, Symbol: symbol} semantic.Node = resolver.hirNode(expr, semantic) + resolver.program.HIR.Expressions = append(resolver.program.HIR.Expressions, semantic) return withExprMeta(expr, semantic), typ } @@ -699,6 +734,31 @@ func (resolver *semanticResolver) fail(err error) { } } +func (resolver *semanticResolver) addDiagnostic(diagnostic SemanticDiagnostic) { + for _, existing := range resolver.program.Diagnostics { + if existing.Code == diagnostic.Code && existing.Span.Start == diagnostic.Span.Start && existing.Message == diagnostic.Message { + return + } + } + resolver.program.Diagnostics = append(resolver.program.Diagnostics, diagnostic) +} + +func isSemanticBuiltin(name string) bool { + return semanticBuiltins[name] +} + +var semanticBuiltins = map[string]bool{ + "println": true, "runCatching": true, "Channel": true, + "listOf": true, "mutableListOf": true, "mapOf": true, "mutableMapOf": true, + "append": true, "keys": true, "goAssert": true, "len": true, "cap": true, + "make": true, "new": true, "copy": true, "delete": true, "close": true, + "panic": true, "recover": true, "string": true, "int": true, "float64": true, "bool": true, + "sql": true, "set": true, "now": true, "Result": true, "ByteSlice": true, + "runBlocking": true, "coroutineScope": true, "launch": true, "async": true, + "delay": true, "withTimeout": true, "isActive": true, + "continue": true, "break": true, +} + func (resolver *semanticResolver) validateEnumExpression(expr Expr, meaning ExprMeaning) { if meaning != EnumConstructionExpr { return @@ -745,6 +805,30 @@ func (resolver *semanticResolver) validateEnumExpression(expr Expr, meaning Expr } } +func (resolver *semanticResolver) validateGenericCall(expr Expr) { + call, ok := expr.(CallExpr) + if !ok { + return + } + ident, ok := call.Callee.(IdentExpr) + if !ok { + return + } + var params []string + found := false + if function, ok := resolver.program.Functions[ident.Name]; ok { + params = function.TypeParams + found = true + } + if class, ok := resolver.program.ClassInfo[ident.Name]; ok { + params = class.TypeParams + found = true + } + if found && len(call.TypeArgs) > 0 && len(call.TypeArgs) != len(params) { + resolver.fail(fmt.Errorf("%s expects %d type arguments, got %d", ident.Name, len(params), len(call.TypeArgs))) + } +} + func collectionElement(typ Type) Type { if generic, ok := typ.(GenericType); ok && len(generic.Args) > 0 { return generic.Args[len(generic.Args)-1] diff --git a/internal/lang/parser.go b/internal/lang/parser.go index a2fbebb..91798c9 100644 --- a/internal/lang/parser.go +++ b/internal/lang/parser.go @@ -17,7 +17,14 @@ func Parse(input string) (*Program, error) { return nil, err } p := &parser{tokens: tokens} - return p.parseProgram() + program, err := p.parseProgram() + if err != nil { + return nil, err + } + if err := hydrateTypeRefs(program); err != nil { + return nil, err + } + return program, nil } func (p *parser) parseProgram() (*Program, error) { @@ -292,6 +299,10 @@ func (p *parser) parseClass() (ClassDecl, error) { if err != nil { return ClassDecl{}, err } + typeParams, err := p.parseTypeParameters() + if err != nil { + return ClassDecl{}, err + } var fields []FieldDecl if p.match(tokenLParen) { fields, err = p.parseClassFields() @@ -308,7 +319,7 @@ func (p *parser) parseClass() (ClassDecl, error) { return ClassDecl{}, err } if !p.match(tokenLBrace) { - return ClassDecl{Name: name.lexeme, Data: data, Fields: fields, Parents: parents, Methods: nil}, nil + return ClassDecl{Name: name.lexeme, TypeParams: typeParams, Data: data, Fields: fields, Parents: parents, Methods: nil}, nil } var methods []FunctionDecl @@ -329,7 +340,7 @@ func (p *parser) parseClass() (ClassDecl, error) { return ClassDecl{}, err } - return ClassDecl{Name: name.lexeme, Data: data, Fields: fields, Parents: parents, Methods: methods}, nil + return ClassDecl{Name: name.lexeme, TypeParams: typeParams, Data: data, Fields: fields, Parents: parents, Methods: methods}, nil } func (p *parser) parseClassParents() ([]string, error) { @@ -467,6 +478,7 @@ func (p *parser) parseFunction() (FunctionDecl, error) { return FunctionDecl{ Name: signature.Name, + TypeParams: signature.TypeParams, Params: signature.Params, ReturnType: signature.ReturnType, Body: body, @@ -483,6 +495,10 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) { if err != nil { return FunctionSignature{}, err } + typeParams, err := p.parseTypeParameters() + if err != nil { + return FunctionSignature{}, err + } if _, err := p.expect(tokenLParen, "expected '('"); err != nil { return FunctionSignature{}, err } @@ -505,12 +521,39 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) { return FunctionSignature{ Name: name.lexeme, + TypeParams: typeParams, Params: params, ReturnType: returnType, Suspend: suspend, }, nil } +func (p *parser) parseTypeParameters() ([]string, error) { + if !p.match(tokenLt) { + return nil, nil + } + var params []string + seen := map[string]bool{} + for { + name, err := p.expect(tokenIdent, "expected type parameter") + if err != nil { + return nil, err + } + if seen[name.lexeme] { + return nil, fmt.Errorf("duplicate type parameter %s", name.lexeme) + } + seen[name.lexeme] = true + params = append(params, name.lexeme) + if !p.match(tokenComma) { + break + } + } + if _, err := p.expect(tokenGt, "expected '>' after type parameters"); err != nil { + return nil, err + } + return params, nil +} + func (p *parser) parseParams() ([]Param, error) { var params []Param if p.check(tokenRParen) { @@ -528,7 +571,11 @@ func (p *parser) parseParams() ([]Param, error) { if err != nil { return nil, err } - params = append(params, Param{Name: name.lexeme, Type: typ}) + ref, err := ParseTypeRef(typ) + if err != nil { + return nil, err + } + params = append(params, Param{Name: name.lexeme, Type: typ, TypeRef: ref}) if !p.match(tokenComma) { return params, nil } @@ -832,7 +879,7 @@ func (p *parser) parseVarDecl(mutable bool) (Stmt, error) { return nil, err } if len(names) == 1 { - return VarDecl{Mutable: mutable, Name: name, Type: typ, Value: value}, nil + return VarDecl{Mutable: mutable, Name: name, Pos: names[0].pos, Type: typ, Value: value}, nil } decl := MultiVarDecl{Mutable: mutable, Names: make([]string, 0, len(names)), Value: value} for _, name := range names { @@ -908,7 +955,7 @@ func (p *parser) parsePrefix() (Expr, error) { tok := p.advance() switch tok.kind { case tokenIdent: - return p.parsePostfix(IdentExpr{Name: tok.lexeme}) + return p.parsePostfix(IdentExpr{Name: tok.lexeme, Pos: tok.pos}) case tokenInt: return IntExpr{Value: tok.lexeme}, nil case tokenFloat: @@ -999,6 +1046,10 @@ func (p *parser) parseLambdaParams() ([]Param, bool, error) { return nil, false, err } param.Type = typ + param.TypeRef, err = ParseTypeRef(typ) + if err != nil { + return nil, false, err + } } params = append(params, param) if !p.match(tokenComma) { @@ -1028,13 +1079,13 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) { if !selectorName(name.lexeme) { return nil, fmt.Errorf("expected selector name at %d, found %q", name.pos, name.lexeme) } - expr = SelectorExpr{Receiver: expr, Name: name.lexeme} + expr = SelectorExpr{Receiver: expr, Name: name.lexeme, NamePos: name.pos} case p.match(tokenSafeDot): name := p.advance() if !selectorName(name.lexeme) { return nil, fmt.Errorf("expected selector name at %d, found %q", name.pos, name.lexeme) } - expr = SafeSelectorExpr{Receiver: expr, Name: name.lexeme} + expr = SafeSelectorExpr{Receiver: expr, Name: name.lexeme, NamePos: name.pos} case p.match(tokenDoubleBang): expr = NonNullExpr{Value: expr} case p.match(tokenQuestion): diff --git a/internal/lang/semantics.go b/internal/lang/semantics.go index 34f7df3..6d764fc 100644 --- a/internal/lang/semantics.go +++ b/internal/lang/semantics.go @@ -33,7 +33,11 @@ func (checker *mutabilityChecker) checkFunction(function *FunctionDecl, class *C _ = checker.scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}}) } for _, param := range function.Params { - typ, _ := checker.semantic.ResolveType(param.Type) + typeParams := append([]string{}, function.TypeParams...) + if class != nil { + typeParams = append(class.TypeParams, typeParams...) + } + typ, _ := checker.semantic.ResolveTypeRefWithParams(param.TypeRef, typeParams) _ = checker.scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ}) } return checker.checkStmts(function.Body) @@ -46,7 +50,7 @@ func (checker *mutabilityChecker) checkStmts(statements []Stmt) error { if err := checker.checkExpr(value.Value); err != nil { return err } - typ, _ := checker.semantic.ResolveType(value.Type) + typ, _ := checker.semantic.ResolveTypeRef(value.TypeRef) _ = checker.scope.Define(&Symbol{Name: value.Name, Kind: VariableSymbol, Type: typ, Mutable: value.Mutable}) case MultiVarDecl: if err := checker.checkExpr(value.Value); err != nil { @@ -214,7 +218,7 @@ func (checker *mutabilityChecker) checkExpr(expr Expr) error { _ = checker.scope.Define(&Symbol{Name: "it", Kind: VariableSymbol, Type: UnknownType{}}) } for _, param := range value.Params { - typ, _ := checker.semantic.ResolveType(param.Type) + typ, _ := checker.semantic.ResolveTypeRef(param.TypeRef) _ = checker.scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ}) } return checker.checkStmts(value.Body) diff --git a/internal/lang/symbols.go b/internal/lang/symbols.go index bd89bfc..9df95df 100644 --- a/internal/lang/symbols.go +++ b/internal/lang/symbols.go @@ -48,26 +48,48 @@ func (scope *Scope) Lookup(name string) (*Symbol, bool) { } type ClassSymbol struct { - Name string - Decl *ClassDecl - Fields map[string]*Symbol - Methods map[string]*Symbol + Name string + TypeParams []string + Decl *ClassDecl + Fields map[string]*Symbol + Methods map[string]*Symbol } type SemanticProgram struct { - Syntax *Program - Global *Scope - Classes map[string]ClassDecl - ClassInfo map[string]*ClassSymbol - Functions map[string]FunctionDecl - Enums map[string]EnumDecl - Imports map[string]bool - GoPackages map[string]*gotypes.Package - HIR *HIRProgram - Mappings *mappingState + Syntax *Program + Global *Scope + Classes map[string]ClassDecl + ClassInfo map[string]*ClassSymbol + Functions map[string]FunctionDecl + Enums map[string]EnumDecl + Imports map[string]bool + GoPackages map[string]*gotypes.Package + HIR *HIRProgram + Mappings *mappingState + Diagnostics []SemanticDiagnostic } func Analyze(program *Program) (*SemanticProgram, error) { + semantic, diagnostics := AnalyzeWithContext(program, nil) + if len(diagnostics) > 0 { + return nil, diagnostics[0] + } + return semantic, nil +} + +func AnalyzeWithContext(program *Program, additional []*Program) (*SemanticProgram, []SemanticDiagnostic) { + semantic, err := analyzeProgram(program, additional) + if err != nil { + return semantic, []SemanticDiagnostic{diagnosticForError("semantic-error", err)} + } + diagnostics := semantic.Diagnostics + if len(diagnostics) > 0 { + return semantic, diagnostics + } + return semantic, nil +} + +func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, error) { semantic := &SemanticProgram{ Syntax: program, Global: NewScope(nil), @@ -79,67 +101,94 @@ func Analyze(program *Program) (*SemanticProgram, error) { GoPackages: map[string]*gotypes.Package{}, Mappings: &mappingState{functions: map[string]string{}}, } - for index := range program.Classes { - decl := &program.Classes[index] - class := &ClassSymbol{Name: decl.Name, Decl: decl, Fields: map[string]*Symbol{}, Methods: map[string]*Symbol{}} - semantic.Classes[decl.Name] = *decl - semantic.ClassInfo[decl.Name] = class - if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: ClassSymbolKind, Type: ClassType{Class: class}, Decl: decl}); err != nil { - return nil, err - } - } - for index := range program.Enums { - decl := &program.Enums[index] - semantic.Enums[decl.Name] = *decl - if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: EnumSymbolKind, Type: NamedType{Name: decl.Name}, Decl: decl}); err != nil { - return nil, err - } - } - for _, imported := range program.Imports { - name := imported.Alias - if name == "" { - name = defaultImportAlias(imported) - } - semantic.Imports[name] = true - if importedPackage, err := importGoPackage(imported); err == nil { - semantic.GoPackages[name] = importedPackage - } - if existing, ok := semantic.Global.Lookup(name); ok && existing.Kind == ImportSymbolKind { - continue - } - if err := semantic.Global.Define(&Symbol{Name: name, Kind: ImportSymbolKind, Type: NamedType{Name: name}, Decl: imported}); err != nil { - return nil, err - } - } - for index := range program.Functions { - decl := &program.Functions[index] - semantic.Functions[decl.Name] = *decl - typ, err := semantic.functionType(decl.Params, decl.ReturnType) - if err != nil { - return nil, fmt.Errorf("function %s: %w", decl.Name, err) - } - if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: FunctionSymbolKind, Type: typ, Decl: decl}); err != nil { - return nil, err - } - } - for index := range program.Classes { - decl := &program.Classes[index] - class := semantic.ClassInfo[decl.Name] - for fieldIndex := range decl.Fields { - field := &decl.Fields[fieldIndex] - typ, err := semantic.ResolveType(field.Type) - if err != nil { - return nil, fmt.Errorf("field %s.%s: %w", decl.Name, field.Name, err) + programs := append([]*Program{program}, additional...) + for _, source := range programs { + for index := range source.Classes { + decl := &source.Classes[index] + class := &ClassSymbol{Name: decl.Name, TypeParams: decl.TypeParams, Decl: decl, Fields: map[string]*Symbol{}, Methods: map[string]*Symbol{}} + semantic.Classes[decl.Name] = *decl + semantic.ClassInfo[decl.Name] = class + if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: ClassSymbolKind, Type: ClassType{Class: class}, Decl: decl}); err != nil { + return nil, err } - class.Fields[field.Name] = &Symbol{Name: field.Name, Kind: VariableSymbol, Type: typ, Mutable: field.Mutable, Decl: field} } - for methodIndex := range decl.Methods { - method := &decl.Methods[methodIndex] - typ, err := semantic.functionType(method.Params, method.ReturnType) - if err != nil { - return nil, fmt.Errorf("method %s.%s: %w", decl.Name, method.Name, err) + } + for _, source := range programs { + for index := range source.Enums { + decl := &source.Enums[index] + semantic.Enums[decl.Name] = *decl + if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: EnumSymbolKind, Type: NamedType{Name: decl.Name}, Decl: decl}); err != nil { + return nil, err + } + } + } + for _, source := range programs { + for _, imported := range source.Imports { + name := imported.Alias + if name == "" { + name = defaultImportAlias(imported) + } + semantic.Imports[name] = true + if importedPackage, err := importGoPackage(imported); err == nil { + semantic.GoPackages[name] = importedPackage + } + if existing, ok := semantic.Global.Lookup(name); ok && existing.Kind == ImportSymbolKind { + continue + } + if err := semantic.Global.Define(&Symbol{Name: name, Kind: ImportSymbolKind, Type: NamedType{Name: name}, Decl: imported}); err != nil { + return nil, err + } + } + } + for _, source := range programs { + for index := range source.Embeds { + embedded := &source.Embeds[index] + typ, err := semantic.ResolveType(embedded.Type) + if err != nil { + return nil, err + } + if _, exists := semantic.Global.Lookup(embedded.Name); !exists { + _ = semantic.Global.Define(&Symbol{Name: embedded.Name, Kind: VariableSymbol, Type: typ, Decl: embedded}) + } + } + } + for _, source := range programs { + for index := range source.Functions { + decl := &source.Functions[index] + semantic.Functions[decl.Name] = *decl + typ, err := semantic.functionType(decl.TypeParams, decl.Params, decl.ReturnType) + if err != nil { + return nil, fmt.Errorf("function %s: %w", decl.Name, err) + } + if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: FunctionSymbolKind, Type: typ, Decl: decl}); err != nil { + return nil, err + } + } + } + for _, source := range programs { + for index := range source.Classes { + decl := &source.Classes[index] + class := semantic.ClassInfo[decl.Name] + for fieldIndex := range decl.Fields { + field := &decl.Fields[fieldIndex] + typ, err := semantic.ResolveTypeRefWithParams(field.TypeRef, decl.TypeParams) + if err != nil { + return nil, fmt.Errorf("field %s.%s: %w", decl.Name, field.Name, err) + } + class.Fields[field.Name] = &Symbol{Name: field.Name, Kind: VariableSymbol, Type: typ, Mutable: field.Mutable, Decl: field} + } + for methodIndex := range decl.Methods { + method := &decl.Methods[methodIndex] + if len(method.TypeParams) > 0 { + return nil, fmt.Errorf("generic methods are not supported; declare type parameters on class %s", decl.Name) + } + methodParams := append(append([]string{}, decl.TypeParams...), method.TypeParams...) + typ, err := semantic.functionType(methodParams, method.Params, method.ReturnType) + if err != nil { + return nil, fmt.Errorf("method %s.%s: %w", decl.Name, method.Name, err) + } + class.Methods[method.Name] = &Symbol{Name: method.Name, Kind: FunctionSymbolKind, Type: typ, Decl: method} } - class.Methods[method.Name] = &Symbol{Name: method.Name, Kind: FunctionSymbolKind, Type: typ, Decl: method} } } if err := validateMutability(semantic); err != nil { @@ -164,6 +213,30 @@ func (semantic *SemanticProgram) ResolveType(text string) (Type, error) { return resolved, nil } +func (semantic *SemanticProgram) ResolveTypeRef(ref TypeRef) (Type, error) { + return semantic.ResolveTypeRefWithParams(ref, nil) +} + +func (semantic *SemanticProgram) ResolveTypeRefWithParams(ref TypeRef, params []string) (Type, error) { + typ := ref.Syntax + if typ == nil { + parsed, err := ParseType(ref.Source) + if err != nil { + return nil, err + } + typ = parsed + } + paramSet := map[string]bool{} + for _, param := range params { + paramSet[param] = true + } + resolved := resolveClassTypes(resolveTypeParameters(typ, paramSet), semantic.ClassInfo) + if err := validateNoClassPointer(resolved); err != nil { + return nil, err + } + return resolved, nil +} + func validateNoClassPointer(typ Type) error { switch value := typ.(type) { case GoPointerType: @@ -190,20 +263,24 @@ func validateNoClassPointer(typ Type) error { return nil } -func (semantic *SemanticProgram) functionType(params []Param, result string) (Type, error) { +func (semantic *SemanticProgram) functionType(typeParams []string, params []Param, result string) (Type, error) { paramTypes := make([]Type, len(params)) for index, param := range params { - typ, err := semantic.ResolveType(param.Type) + typ, err := semantic.ResolveTypeRefWithParams(param.TypeRef, typeParams) if err != nil { return nil, err } paramTypes[index] = typ } - resultType, err := semantic.ResolveType(result) + resultRef, err := ParseTypeRef(result) if err != nil { return nil, err } - return FunctionType{Params: paramTypes, Result: resultType}, nil + resultType, err := semantic.ResolveTypeRefWithParams(resultRef, typeParams) + if err != nil { + return nil, err + } + return FunctionType{TypeParams: typeParams, Params: paramTypes, Result: resultType}, nil } func (semantic *SemanticProgram) GoType(text string) string { diff --git a/internal/lang/type_checker.go b/internal/lang/type_checker.go index d1dc271..a330d3c 100644 --- a/internal/lang/type_checker.go +++ b/internal/lang/type_checker.go @@ -66,6 +66,14 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) return resolve(value.TypeArgs[0]) } receiverType := semantic.TypeOf(selector.Receiver, environment) + if channel, ok := receiverType.(GenericType); ok && channel.Base.String() == "Channel" && len(channel.Args) == 1 { + if selector.Name == "read" { + return channel.Args[0] + } + if selector.Name == "send" { + return NamedType{Name: "Unit"} + } + } if iterator, ok := receiverType.(GenericType); ok && iterator.Base.String() == "GotlinSQLIterator" && len(iterator.Args) == 1 { switch selector.Name { case "next": @@ -78,10 +86,10 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) return NamedType{Name: "Unit"} } } - if class := classTypeOf(receiverType); class != nil { + if class, bindings := classInstance(receiverType); class != nil { if method, ok := class.Methods[selector.Name]; ok { if function, ok := method.Type.(FunctionType); ok { - return function.Result + return substituteType(function.Result, bindings) } } } @@ -90,16 +98,62 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) } } if ident, ok := value.Callee.(IdentExpr); ok { + switch ident.Name { + case "Channel": + if len(value.TypeArgs) == 1 { + return GenericType{Base: NamedType{Name: "Channel"}, Args: []Type{resolve(value.TypeArgs[0])}} + } + case "listOf", "mutableListOf": + element := Type(UnknownType{}) + if len(value.TypeArgs) == 1 { + element = resolve(value.TypeArgs[0]) + } else if len(value.Args) > 0 { + element = semantic.TypeOf(value.Args[0], environment) + } + return GenericType{Base: NamedType{Name: "List"}, Args: []Type{element}} + case "mapOf", "mutableMapOf": + key, item := Type(UnknownType{}), Type(UnknownType{}) + if len(value.TypeArgs) == 2 { + key, item = resolve(value.TypeArgs[0]), resolve(value.TypeArgs[1]) + } else if len(value.Args) >= 2 { + key, item = semantic.TypeOf(value.Args[0], environment), semantic.TypeOf(value.Args[1], environment) + } + return GenericType{Base: NamedType{Name: "Map"}, Args: []Type{key, item}} + case "ByteSlice": + return NamedType{Name: "ByteSlice"} + case "async": + if len(value.TypeArgs) == 1 { + return GenericType{Base: NamedType{Name: "Deferred"}, Args: []Type{resolve(value.TypeArgs[0])}} + } + } if ident.Name == "keys" && len(value.Args) == 1 { if mapping, ok := semantic.TypeOf(value.Args[0], environment).(GenericType); ok && (mapping.Base.String() == "Map" || mapping.Base.String() == "MutableMap") && len(mapping.Args) == 2 { return GenericType{Base: NamedType{Name: "List"}, Args: []Type{mapping.Args[0]}} } } - if function, ok := semantic.Functions[ident.Name]; ok { - return resolve(function.ReturnType) + if symbol, ok := semantic.Global.Lookup(ident.Name); ok && symbol.Kind == FunctionSymbolKind { + if function, ok := symbol.Type.(FunctionType); ok { + bindings := semantic.callTypeBindings(function.TypeParams, function.Params, value.TypeArgs, value.Args, environment) + return substituteType(function.Result, bindings) + } } if class, ok := semantic.ClassInfo[ident.Name]; ok { - return ClassType{Class: class} + if len(class.TypeParams) == 0 { + return ClassType{Class: class} + } + params := make([]Type, 0, len(class.Fields)) + for _, field := range class.Decl.Fields { + params = append(params, class.Fields[field.Name].Type) + } + bindings := semantic.callTypeBindings(class.TypeParams, params, value.TypeArgs, value.Args, environment) + args := make([]Type, len(class.TypeParams)) + for index, name := range class.TypeParams { + args[index] = bindings[name] + if args[index] == nil { + args[index] = TypeParameterType{Name: name} + } + } + return GenericType{Base: ClassType{Class: class}, Args: args} } } if sqlType, ok := sqlChainResultType(value); ok { @@ -121,9 +175,9 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) return NamedType{Name: receiver.Name} } } - if class := classTypeOf(semantic.TypeOf(value.Receiver, environment)); class != nil { + if class, bindings := classInstance(semantic.TypeOf(value.Receiver, environment)); class != nil { if field, ok := class.Fields[value.Name]; ok { - return field.Type + return substituteType(field.Type, bindings) } } if field := semantic.goFieldType(semantic.TypeOf(value.Receiver, environment), value.Name); !isUnknownType(field) { @@ -169,13 +223,43 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) } func classTypeOf(typ Type) *ClassSymbol { + class, _ := classInstance(typ) + return class +} + +func classInstance(typ Type) (*ClassSymbol, map[string]Type) { switch value := typ.(type) { case ClassType: - return value.Class + return value.Class, map[string]Type{} case NullableType: - return classTypeOf(value.Element) + return classInstance(value.Element) + case GenericType: + if class, ok := value.Base.(ClassType); ok { + bindings := map[string]Type{} + for index, name := range class.Class.TypeParams { + if index < len(value.Args) { + bindings[name] = value.Args[index] + } + } + return class.Class, bindings + } } - return nil + return nil, nil +} + +func (semantic *SemanticProgram) callTypeBindings(typeParams []string, params []Type, explicit []string, args []Expr, environment TypeEnvironment) map[string]Type { + bindings := map[string]Type{} + for index, argument := range explicit { + if index < len(typeParams) { + bindings[typeParams[index]] = func() Type { typ, _ := semantic.ResolveType(argument); return typ }() + } + } + for index, argument := range args { + if index < len(params) { + inferTypeBindings(params[index], semantic.TypeOf(argument, environment), bindings) + } + } + return bindings } func nullableSemanticType(typ Type) Type { diff --git a/internal/lang/type_refs.go b/internal/lang/type_refs.go new file mode 100644 index 0000000..60a5faa --- /dev/null +++ b/internal/lang/type_refs.go @@ -0,0 +1,248 @@ +package lang + +import "fmt" + +func hydrateTypeRefs(program *Program) error { + for index := range program.Embeds { + ref, err := ParseTypeRef(program.Embeds[index].Type) + if err != nil { + return err + } + program.Embeds[index].TypeRef = ref + } + for enumIndex := range program.Enums { + for variantIndex := range program.Enums[enumIndex].Variants { + variant := &program.Enums[enumIndex].Variants[variantIndex] + variant.PayloadRefs = make([]TypeRef, len(variant.PayloadTypes)) + for index, text := range variant.PayloadTypes { + ref, err := ParseTypeRef(text) + if err != nil { + return fmt.Errorf("enum %s.%s: %w", program.Enums[enumIndex].Name, variant.Name, err) + } + variant.PayloadRefs[index] = ref + } + } + } + for interfaceIndex := range program.Interfaces { + for methodIndex := range program.Interfaces[interfaceIndex].Methods { + if err := hydrateSignatureRefs(&program.Interfaces[interfaceIndex].Methods[methodIndex]); err != nil { + return err + } + } + } + for classIndex := range program.Classes { + class := &program.Classes[classIndex] + for fieldIndex := range class.Fields { + ref, err := ParseTypeRef(class.Fields[fieldIndex].Type) + if err != nil { + return fmt.Errorf("field %s.%s: %w", class.Name, class.Fields[fieldIndex].Name, err) + } + class.Fields[fieldIndex].TypeRef = ref + } + for methodIndex := range class.Methods { + if err := hydrateFunctionRefs(&class.Methods[methodIndex]); err != nil { + return err + } + } + } + for index := range program.Functions { + if err := hydrateFunctionRefs(&program.Functions[index]); err != nil { + return err + } + } + return nil +} + +func hydrateSignatureRefs(signature *FunctionSignature) error { + for index := range signature.Params { + ref, err := ParseTypeRef(signature.Params[index].Type) + if err != nil { + return err + } + signature.Params[index].TypeRef = ref + } + ref, err := ParseTypeRef(signature.ReturnType) + if err != nil { + return err + } + signature.ReturnRef = ref + return nil +} + +func hydrateFunctionRefs(function *FunctionDecl) error { + for index := range function.Params { + ref, err := ParseTypeRef(function.Params[index].Type) + if err != nil { + return err + } + function.Params[index].TypeRef = ref + } + ref, err := ParseTypeRef(function.ReturnType) + if err != nil { + return err + } + function.ReturnRef = ref + return hydrateStmtTypeRefs(function.Body) +} + +func hydrateStmtTypeRefs(statements []Stmt) error { + for index, statement := range statements { + switch value := statement.(type) { + case VarDecl: + ref, err := ParseTypeRef(value.Type) + if err != nil { + return err + } + value.TypeRef = ref + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + statements[index] = value + case MultiVarDecl: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + case AssignStmt: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + case AddAssignStmt: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + case MultiAssignStmt: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + case ReturnStmt: + if value.Value != nil { + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + } + case ThrowStmt: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + case DeferStmt: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + case ExprStmt: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + case IfStmt: + if err := hydrateExprTypeRefs(value.Cond); err != nil { + return err + } + if err := hydrateStmtTypeRefs(value.Then); err != nil { + return err + } + if err := hydrateStmtTypeRefs(value.Else); err != nil { + return err + } + case WhileStmt: + if err := hydrateExprTypeRefs(value.Cond); err != nil { + return err + } + if err := hydrateStmtTypeRefs(value.Body); err != nil { + return err + } + case ForEachStmt: + if err := hydrateExprTypeRefs(value.Source); err != nil { + return err + } + if err := hydrateStmtTypeRefs(value.Body); err != nil { + return err + } + case MatchStmt: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + for _, matchCase := range value.Cases { + if err := hydrateStmtTypeRefs(matchCase.Body); err != nil { + return err + } + } + case TryCatchStmt: + ref, err := ParseTypeRef(value.CatchType) + if err != nil { + return err + } + value.CatchRef = ref + statements[index] = value + if err := hydrateStmtTypeRefs(value.TryBody); err != nil { + return err + } + if err := hydrateStmtTypeRefs(value.CatchBody); err != nil { + return err + } + } + } + return nil +} + +func hydrateExprTypeRefs(expression Expr) error { + switch value := expression.(type) { + case UnaryExpr: + return hydrateExprTypeRefs(value.Value) + case NonNullExpr: + return hydrateExprTypeRefs(value.Value) + case TryExpr: + return hydrateExprTypeRefs(value.Value) + case BinaryExpr: + if err := hydrateExprTypeRefs(value.Left); err != nil { + return err + } + return hydrateExprTypeRefs(value.Right) + case CallExpr: + if err := hydrateExprTypeRefs(value.Callee); err != nil { + return err + } + for _, argument := range value.Args { + if err := hydrateExprTypeRefs(argument); err != nil { + return err + } + } + for _, argument := range value.NamedArgs { + if err := hydrateExprTypeRefs(argument.Value); err != nil { + return err + } + } + case SelectorExpr: + return hydrateExprTypeRefs(value.Receiver) + case SafeSelectorExpr: + return hydrateExprTypeRefs(value.Receiver) + case IndexExpr: + if err := hydrateExprTypeRefs(value.Receiver); err != nil { + return err + } + return hydrateExprTypeRefs(value.Index) + case EnumVariantExpr: + for _, item := range value.Values { + if err := hydrateExprTypeRefs(item); err != nil { + return err + } + } + case MatchExpr: + if err := hydrateExprTypeRefs(value.Value); err != nil { + return err + } + for _, matchCase := range value.Cases { + if err := hydrateExprTypeRefs(matchCase.Value); err != nil { + return err + } + } + case LambdaExpr: + for index := range value.Params { + ref, err := ParseTypeRef(value.Params[index].Type) + if err != nil { + return err + } + value.Params[index].TypeRef = ref + } + return hydrateStmtTypeRefs(value.Body) + } + return nil +} diff --git a/internal/lang/types.go b/internal/lang/types.go index 1c00d75..5883fbc 100644 --- a/internal/lang/types.go +++ b/internal/lang/types.go @@ -10,6 +10,19 @@ type Type interface { String() string } +type TypeRef struct { + Source string + Syntax Type +} + +func ParseTypeRef(source string) (TypeRef, error) { + typ, err := ParseType(source) + if err != nil { + return TypeRef{}, err + } + return TypeRef{Source: source, Syntax: typ}, nil +} + type UnknownType struct{} func (UnknownType) typeNode() {} @@ -20,6 +33,11 @@ type NamedType struct{ Name string } func (NamedType) typeNode() {} func (t NamedType) String() string { return t.Name } +type TypeParameterType struct{ Name string } + +func (TypeParameterType) typeNode() {} +func (t TypeParameterType) String() string { return t.Name } + type ClassType struct{ Class *ClassSymbol } func (ClassType) typeNode() {} @@ -36,8 +54,9 @@ func (GoPointerType) typeNode() {} func (t GoPointerType) String() string { return "*" + t.Element.String() } type FunctionType struct { - Params []Type - Result Type + TypeParams []string + Params []Type + Result Type } func (FunctionType) typeNode() {} @@ -54,6 +73,17 @@ type GenericType struct { Args []Type } +type TupleType struct{ Elements []Type } + +func (TupleType) typeNode() {} +func (t TupleType) String() string { + elements := make([]string, len(t.Elements)) + for index, element := range t.Elements { + elements[index] = element.String() + } + return "(" + strings.Join(elements, ", ") + ")" +} + func (GenericType) typeNode() {} func (t GenericType) String() string { args := make([]string, 0, len(t.Args)) @@ -125,6 +155,8 @@ func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type { return ClassType{Class: class} } return value + case TypeParameterType: + return value case NullableType: return NullableType{Element: resolveClassTypes(value.Element, classes)} case GoPointerType: @@ -141,6 +173,46 @@ func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type { args[i] = resolveClassTypes(arg, classes) } return GenericType{Base: resolveClassTypes(value.Base, classes), Args: args} + case TupleType: + elements := make([]Type, len(value.Elements)) + for index, element := range value.Elements { + elements[index] = resolveClassTypes(element, classes) + } + return TupleType{Elements: elements} + default: + return typ + } +} + +func resolveTypeParameters(typ Type, params map[string]bool) Type { + switch value := typ.(type) { + case NamedType: + if params[value.Name] { + return TypeParameterType{Name: value.Name} + } + return value + case NullableType: + return NullableType{Element: resolveTypeParameters(value.Element, params)} + case GoPointerType: + return GoPointerType{Element: resolveTypeParameters(value.Element, params)} + case FunctionType: + resolved := make([]Type, len(value.Params)) + for index, param := range value.Params { + resolved[index] = resolveTypeParameters(param, params) + } + return FunctionType{TypeParams: value.TypeParams, Params: resolved, Result: resolveTypeParameters(value.Result, params)} + case GenericType: + args := make([]Type, len(value.Args)) + for index, arg := range value.Args { + args[index] = resolveTypeParameters(arg, params) + } + return GenericType{Base: resolveTypeParameters(value.Base, params), Args: args} + case TupleType: + elements := make([]Type, len(value.Elements)) + for index, element := range value.Elements { + elements[index] = resolveTypeParameters(element, params) + } + return TupleType{Elements: elements} default: return typ } @@ -148,6 +220,63 @@ func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type { func typeEqual(left, right Type) bool { return left.String() == right.String() } +func substituteType(typ Type, bindings map[string]Type) Type { + switch value := typ.(type) { + case TypeParameterType: + if bound, ok := bindings[value.Name]; ok { + return bound + } + return value + case NullableType: + return NullableType{Element: substituteType(value.Element, bindings)} + case GoPointerType: + return GoPointerType{Element: substituteType(value.Element, bindings)} + case GenericType: + args := make([]Type, len(value.Args)) + for index, arg := range value.Args { + args[index] = substituteType(arg, bindings) + } + return GenericType{Base: substituteType(value.Base, bindings), Args: args} + case TupleType: + elements := make([]Type, len(value.Elements)) + for index, element := range value.Elements { + elements[index] = substituteType(element, bindings) + } + return TupleType{Elements: elements} + case FunctionType: + params := make([]Type, len(value.Params)) + for index, param := range value.Params { + params[index] = substituteType(param, bindings) + } + return FunctionType{TypeParams: value.TypeParams, Params: params, Result: substituteType(value.Result, bindings)} + default: + return typ + } +} + +func inferTypeBindings(parameter, argument Type, bindings map[string]Type) { + switch expected := parameter.(type) { + case TypeParameterType: + if _, exists := bindings[expected.Name]; !exists && !isUnknownType(argument) { + bindings[expected.Name] = argument + } + case NullableType: + if actual, ok := argument.(NullableType); ok { + inferTypeBindings(expected.Element, actual.Element, bindings) + } + case GenericType: + actual, ok := argument.(GenericType) + if !ok || expected.Base.String() != actual.Base.String() { + return + } + for index := range expected.Args { + if index < len(actual.Args) { + inferTypeBindings(expected.Args[index], actual.Args[index], bindings) + } + } + } +} + func renderGoType(typ Type) string { switch value := typ.(type) { case UnknownType: @@ -201,7 +330,12 @@ func renderGoType(typ Type) string { } return "*GotlinSQLIterator[" + argument + "]" } + if class, ok := value.Base.(ClassType); ok { + return "*" + class.Class.Name + "[" + strings.Join(args, ", ") + "]" + } return base + "[" + strings.Join(args, ", ") + "]" + case TypeParameterType: + return value.Name case NamedType: switch value.Name { case "Int": @@ -225,6 +359,12 @@ func renderGoType(typ Type) string { default: return value.Name } + case TupleType: + elements := make([]string, len(value.Elements)) + for index, element := range value.Elements { + elements[index] = renderGoType(element) + } + return "(" + strings.Join(elements, ", ") + ")" default: return "" } diff --git a/internal/lang/types_test.go b/internal/lang/types_test.go index e5fbae1..ea582be 100644 --- a/internal/lang/types_test.go +++ b/internal/lang/types_test.go @@ -35,6 +35,9 @@ fun create(): Service { return Service(Repository()) }`) if program.Classes[1].Fields[0].Type != "Repository" || program.Functions[0].ReturnType != "Service" { t.Fatalf("syntax types were rewritten: %#v %#v", program.Classes[1].Fields[0], program.Functions[0]) } + if _, ok := program.Classes[1].Fields[0].TypeRef.Syntax.(NamedType); !ok { + t.Fatalf("field syntax TypeRef was not parsed: %#v", program.Classes[1].Fields[0].TypeRef) + } if _, err := GenerateGo(program); err != nil { t.Fatal(err) } @@ -67,6 +70,9 @@ fun parse(value: String): Result { if semantic.HIR == nil || len(semantic.HIR.Functions) != 1 { t.Fatal("typed HIR function was not created") } + if len(semantic.HIR.Expressions) == 0 { + t.Fatal("standalone HIR expression arena is empty") + } decl := program.Functions[0].Body[0].(VarDecl) attempt := decl.Value.(TryExpr) if attempt.Meta.Semantic == nil || attempt.Meta.Semantic.Meaning != PropagateResultExpr || attempt.Meta.Semantic.Type.String() != "Int" { @@ -75,6 +81,9 @@ fun parse(value: String): Result { if _, ok := attempt.Meta.Semantic.Node.(HIRPropagateResult); !ok { t.Fatalf("propagation did not lower to HIRPropagateResult: %#v", attempt.Meta.Semantic.Node) } + if semantic.HIR.Expressions[attempt.Meta.Semantic.ID-1] != attempt.Meta.Semantic { + t.Fatal("syntax annotation does not reference the HIR-owned expression") + } call := attempt.Value.(CallExpr) if call.Meta.Semantic == nil || call.Meta.Semantic.Meaning != GoCallExpr || call.Meta.Semantic.Type.String() != "Result" { t.Fatalf("external call was not resolved: %#v", call.Meta.Semantic) diff --git a/tools/vscode-gotlin/scripts/validate.js b/tools/vscode-gotlin/scripts/validate.js index bd4d9fd..3994241 100644 --- a/tools/vscode-gotlin/scripts/validate.js +++ b/tools/vscode-gotlin/scripts/validate.js @@ -82,7 +82,7 @@ for (const declaration of [ const prefixes = new Set(Object.values(snippets).map((snippet) => snippet.prefix)); for (const prefix of [ - "dataclass", "tablerow", "embed", "coroutinescope", "launch", "async", "enum", "match", "matchvalue", "mapto", "safe", "nonnull", "resultfun", "resultmatch", "defer", "foreach", "sqlfetch", + "dataclass", "genericfun", "genericclass", "tablerow", "embed", "coroutinescope", "launch", "async", "enum", "match", "matchvalue", "mapto", "safe", "nonnull", "resultfun", "resultmatch", "defer", "foreach", "sqlfetch", "sqlsingle", "sqliterator", "sqlinsertnothing", "sqlinsertupdate", "sqlupdatereturning" ]) { assert(prefixes.has(prefix), `snippets are missing prefix ${prefix}`); diff --git a/tools/vscode-gotlin/snippets/gotlin.code-snippets b/tools/vscode-gotlin/snippets/gotlin.code-snippets index 477320a..895d2cf 100644 --- a/tools/vscode-gotlin/snippets/gotlin.code-snippets +++ b/tools/vscode-gotlin/snippets/gotlin.code-snippets @@ -36,6 +36,22 @@ ], "description": "Gotlin data class" }, + "Generic Function": { + "prefix": "genericfun", + "body": [ + "fun ${1:identity}<${2:T}>(${3:value}: ${2:T}): ${2:T} {", + " return ${3:value}", + "}" + ], + "description": "Gotlin generic function" + }, + "Generic Data Class": { + "prefix": "genericclass", + "body": [ + "data class ${1:Box}<${2:T}>(var ${3:value}: ${2:T})" + ], + "description": "Gotlin generic data class" + }, "SQL Table Row": { "prefix": "tablerow", "body": [