Move LSP semantics into typed analysis

This commit is contained in:
pavel 2026-08-27 21:28:11 +02:00
commit bac1183593
26 changed files with 1232 additions and 637 deletions

View file

@ -19,6 +19,7 @@ the semantic Type-to-Go mapping turns a class such as `User` into `*User`.
- `val` and `var` - `val` and `var`
- `Int`, `Long`, `String`, `Boolean`, `Unit` - `Int`, `Long`, `String`, `Boolean`, `Unit`
- function types like `(String) -> 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)` - `if`, `else`, `while`, and `for (item in items)`
- function calls - function calls
- lambdas like `{ x: Int -> println(x) }` and `{ println(it) }` - 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 every arm to return the same type. Block-style statement matches remain
available for side effects. 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<T>(var value: T) {
fun get(): T { return value }
}
fun identity<T>(value: T): T { return value }
val number = identity(42)
val text = identity<String>("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 ## Null safety
Types are non-nullable by default. Add `?` explicitly when `null` is valid: Types are non-nullable by default. Add `?` explicitly when `null` is valid:

View file

@ -13,7 +13,7 @@ Current support:
- publish diagnostics from the existing Gotlin parser/code generator - publish diagnostics from the existing Gotlin parser/code generator
- `textDocument/hover` for packages, imports, and top-level functions - `textDocument/hover` for packages, imports, and top-level functions
- `textDocument/definition` for imported namespaces 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 - optional fallback to `gopls` for hover/definition on Go-imported symbols
Build: Build:

View file

@ -160,6 +160,7 @@ type diagnostic struct {
Range rng `json:"range"` Range rng `json:"range"`
Severity int `json:"severity,omitempty"` Severity int `json:"severity,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Code string `json:"code,omitempty"`
Message string `json:"message"` Message string `json:"message"`
} }
@ -306,13 +307,13 @@ func buildDocumentState(text string) documentState {
return state return state
} }
state.program = program state.program = program
if _, err := lang.Analyze(program); err != nil { _, semanticDiagnostics := lang.AnalyzeWithContext(program, nil)
state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err))
}
state.symbols = indexSymbols(text, program) state.symbols = indexSymbols(text, program)
state.diagnostics = semanticDiagnostics(text, program, state.symbols) state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics)
if _, err := lang.GenerateGo(program); err != nil { if len(semanticDiagnostics) == 0 {
state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err)) if _, err := lang.GenerateGo(program); err != nil {
state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err))
}
} }
return state return state
} }
@ -325,10 +326,11 @@ func (s *server) buildDocumentState(uri, text string) documentState {
return state return state
} }
state.program = program state.program = program
state.symbols = indexSymbols(text, program)
programs, symbols := s.packageContext(uri, program.PackagePath, text) programs, symbols := s.packageContext(uri, program.PackagePath, text)
_, semanticDiagnostics := lang.AnalyzeWithContext(program, programs)
state.symbols = indexSymbols(text, program)
state.packageSymbols = symbols state.packageSymbols = symbols
state.diagnostics = semanticDiagnosticsWithPackage(text, program, state.symbols, programs) state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics)
return state return state
} }
@ -363,7 +365,9 @@ func (s *server) packageContext(currentURI, packagePath, currentText string) ([]
if parseErr != nil || program.PackagePath != packagePath { if parseErr != nil || program.PackagePath != packagePath {
return nil return nil
} }
programs = append(programs, program) if candidateURI != currentURI {
programs = append(programs, program)
}
for _, sym := range indexSymbols(text, program) { for _, sym := range indexSymbols(text, program) {
sym.URI = candidateURI sym.URI = candidateURI
symbols = append(symbols, sym) symbols = append(symbols, sym)
@ -729,227 +733,22 @@ func indexSymbols(text string, program *lang.Program) []symbol {
return symbols return symbols
} }
func semanticDiagnostics(text string, program *lang.Program, symbols []symbol) []diagnostic { func diagnosticsFromSemantic(text string, semantic []lang.SemanticDiagnostic) []diagnostic {
return semanticDiagnosticsWithPackage(text, program, symbols, nil) diagnostics := make([]diagnostic, 0, len(semantic))
} for _, item := range semantic {
start := offsetToPosition(text, item.Span.Start)
func semanticDiagnosticsWithPackage(text string, program *lang.Program, symbols []symbol, packagePrograms []*lang.Program) []diagnostic { endOffset := item.Span.End
var diagnostics []diagnostic if endOffset <= item.Span.Start {
endOffset = item.Span.Start + 1
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
}
} }
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 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 { func offsetToPosition(text string, offset int) position {
runes := []rune(text) runes := []rune(text)
if offset < 0 { if offset < 0 {
@ -1221,7 +1001,11 @@ func renderImportDetail(imp lang.ImportDecl) string {
} }
func renderFunctionSignature(fn lang.FunctionDecl) 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 { func renderFunctionSignatureFromParts(name string, fnParams []lang.Param, returnType string) string {
@ -1244,6 +1028,11 @@ func renderClassSignature(decl lang.ClassDecl) string {
var b strings.Builder var b strings.Builder
b.WriteString("class ") b.WriteString("class ")
b.WriteString(decl.Name) b.WriteString(decl.Name)
if len(decl.TypeParams) > 0 {
b.WriteString("<")
b.WriteString(strings.Join(decl.TypeParams, ", "))
b.WriteString(">")
}
var fields []string var fields []string
for _, field := range decl.Fields { for _, field := range decl.Fields {
@ -1289,289 +1078,59 @@ func renderVariableSignature(name string, mutable bool, typ string) string {
} }
func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo { func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
functions := map[string]lang.FunctionDecl{} var result []variableDeclInfo
for _, fn := range program.Functions { var walk func([]lang.Stmt)
functions[fn.Name] = fn walk = func(statements []lang.Stmt) {
} for _, statement := range statements {
classes := map[string]lang.ClassDecl{} switch value := statement.(type) {
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<any>"}
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<any>"}
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<any, any>"}
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) {
case lang.VarDecl: case lang.VarDecl:
typ := s.Type typ := value.Type
if typ == "" { if typ == "" {
resolved := lang.ResolvedType(s.Value) resolved := lang.ResolvedType(value.Value)
if resolved.String() != "<unknown>" { if resolved.String() != "<unknown>" {
typ = resolved.String() typ = resolved.String()
} else {
typ = inferExprType(s.Value, scope)
} }
} }
out = append(out, variableDeclInfo{Name: s.Name, Mutable: s.Mutable, Type: typ}) result = append(result, variableDeclInfo{Name: value.Name, Mutable: value.Mutable, Type: typ})
scope[s.Name] = typ
case lang.MultiVarDecl: case lang.MultiVarDecl:
types := []string(nil) types := make([]string, len(value.Names))
if call, ok := s.Value.(lang.CallExpr); ok { if fallible, ok := lang.ResolvedType(value.Value).(lang.GenericType); ok && fallible.Base.String() == "Result" && len(fallible.Args) == 2 && len(types) == 2 {
types = inferCallReturnTypes(call, scope) types[0] = fallible.Args[0].String()
} types[1] = fallible.Args[1].String() + "?"
for i, name := range s.Names { } else if tuple, ok := lang.ResolvedType(value.Value).(lang.TupleType); ok && len(tuple.Elements) == len(types) {
typ := "" for index, element := range tuple.Elements {
if i < len(types) { types[index] = element.String()
typ = types[i]
} }
out = append(out, variableDeclInfo{Name: name, Mutable: s.Mutable, Type: typ})
scope[name] = typ
} }
case lang.AddAssignStmt: for index, name := range value.Names {
if _, ok := scope[s.Name]; !ok { result = append(result, variableDeclInfo{Name: name, Mutable: value.Mutable, Type: types[index]})
scope[s.Name] = ""
} }
case lang.IfStmt: case lang.IfStmt:
thenScope := copyTypeScope(scope) walk(value.Then)
elseScope := copyTypeScope(scope) walk(value.Else)
walkStmts(s.Then, thenScope)
walkStmts(s.Else, elseScope)
case lang.WhileStmt: case lang.WhileStmt:
bodyScope := copyTypeScope(scope) walk(value.Body)
walkStmts(s.Body, bodyScope) case lang.ForEachStmt:
walk(value.Body)
case lang.MatchStmt:
for _, matchCase := range value.Cases {
walk(matchCase.Body)
}
case lang.TryCatchStmt: case lang.TryCatchStmt:
tryScope := copyTypeScope(scope) walk(value.TryBody)
walkStmts(s.TryBody, tryScope) walk(value.CatchBody)
catchScope := copyTypeScope(scope)
catchScope[s.CatchName] = s.CatchType
walkStmts(s.CatchBody, catchScope)
} }
} }
} }
for _, function := range program.Functions {
for _, fn := range program.Functions { walk(function.Body)
scope := map[string]string{}
for _, param := range fn.Params {
scope[param.Name] = param.Type
}
walkStmts(fn.Body, scope)
} }
for _, class := range program.Classes { for _, class := range program.Classes {
fieldScope := map[string]string{}
for _, field := range class.Fields {
fieldScope[field.Name] = field.Type
}
for _, method := range class.Methods { for _, method := range class.Methods {
scope := copyTypeScope(fieldScope) walk(method.Body)
for _, param := range method.Params {
scope[param.Name] = param.Type
}
walkStmts(method.Body, scope)
} }
} }
return out return result
}
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
}
} }
func resolveStdlibTarget(state documentState, pos position) (stdlibTarget, bool) { func resolveStdlibTarget(state documentState, pos position) (stdlibTarget, bool) {

View file

@ -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) { func TestBuildDocumentStateInfersHttpServerVariableTypes(t *testing.T) {
text := strings.TrimSpace(` text := strings.TrimSpace(`
package demo package demo
@ -460,7 +475,7 @@ import github.com.uptrace.bun
class C(val db: *bun.DB) { class C(val db: *bun.DB) {
fun run() { fun run() {
val ctx = context.Background() val ctx = context.Background()
val total = db.NewSelect().ColumnExpr("1").Count(ctx) val total = db.NewSelect().ColumnExpr("1").Count(ctx).unwrap()
println(total) println(total)
} }
} }

15
examples/generics.gt Normal file
View file

@ -0,0 +1,15 @@
package main
data class Box<T>(var value: T) {
fun get(): T { return value }
}
fun identity<T>(value: T): T { return value }
fun main() {
val number = identity(42)
val text = identity<String>("value")
val box = Box(text)
println(number)
println(box.get())
}

View file

@ -30,7 +30,7 @@ class EpicControllerImpl(val db: *bun.DB) {
db.NewCreateTable().Model(model).IfNotExists().Exec(ctx) db.NewCreateTable().Model(model).IfNotExists().Exec(ctx)
val user = BunUser("user-from-gotlin") val user = BunUser("user-from-gotlin")
db.NewInsert().Model(user).Exec(ctx) 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) fmt.Fprintln(w, "bun users total:", total)
} }

10
go.mod
View file

@ -2,7 +2,10 @@ module gotlin
go 1.25.6 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 ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect 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 v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/mod v0.39.0 // indirect
golang.org/x/sys v0.41.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 golang.org/x/text v0.34.0 // indirect
mellium.im/sasl v0.3.2 // indirect mellium.im/sasl v0.3.2 // indirect
) )

7
go.sum
View file

@ -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= 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 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= 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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 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/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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=

View file

@ -10,7 +10,10 @@ type Program struct {
Embeds []EmbedDecl Embeds []EmbedDecl
} }
type EmbedDecl struct{ Path, Name, Type string } type EmbedDecl struct {
Path, Name, Type string
TypeRef TypeRef
}
type EnumDecl struct { type EnumDecl struct {
Name string Name string
@ -20,6 +23,7 @@ type EnumDecl struct {
type EnumVariant struct { type EnumVariant struct {
Name string Name string
PayloadTypes []string PayloadTypes []string
PayloadRefs []TypeRef
StringValue string StringValue string
} }
@ -35,6 +39,7 @@ type InterfaceDecl struct {
type ClassDecl struct { type ClassDecl struct {
Name string Name string
TypeParams []string
Data bool Data bool
JSONNaming string JSONNaming string
Table string Table string
@ -48,6 +53,7 @@ type FieldDecl struct {
Private bool Private bool
Name string Name string
Type string Type string
TypeRef TypeRef
Column string Column string
ID bool ID bool
Generated bool Generated bool
@ -55,22 +61,27 @@ type FieldDecl struct {
type FunctionSignature struct { type FunctionSignature struct {
Name string Name string
TypeParams []string
Params []Param Params []Param
ReturnType string ReturnType string
ReturnRef TypeRef
Suspend bool Suspend bool
} }
type FunctionDecl struct { type FunctionDecl struct {
Name string Name string
TypeParams []string
Params []Param Params []Param
ReturnType string ReturnType string
ReturnRef TypeRef
Body []Stmt Body []Stmt
Suspend bool Suspend bool
} }
type Param struct { type Param struct {
Name string Name string
Type string Type string
TypeRef TypeRef
} }
type Stmt interface { type Stmt interface {
@ -84,7 +95,9 @@ type Expr interface {
type VarDecl struct { type VarDecl struct {
Mutable bool Mutable bool
Name string Name string
Pos int
Type string Type string
TypeRef TypeRef
Value Expr Value Expr
} }
@ -173,6 +186,7 @@ type TryCatchStmt struct {
TryBody []Stmt TryBody []Stmt
CatchName string CatchName string
CatchType string CatchType string
CatchRef TypeRef
CatchBody []Stmt CatchBody []Stmt
} }
@ -208,6 +222,7 @@ type MatchExprCase struct {
type IdentExpr struct { type IdentExpr struct {
Meta ExprMeta Meta ExprMeta
Name string Name string
Pos int
} }
func (IdentExpr) exprNode() {} func (IdentExpr) exprNode() {}
@ -280,6 +295,7 @@ type SelectorExpr struct {
Meta ExprMeta Meta ExprMeta
Receiver Expr Receiver Expr
Name string Name string
NamePos int
} }
func (SelectorExpr) exprNode() {} func (SelectorExpr) exprNode() {}
@ -288,6 +304,7 @@ type SafeSelectorExpr struct {
Meta ExprMeta Meta ExprMeta
Receiver Expr Receiver Expr
Name string Name string
NamePos int
} }
func (SafeSelectorExpr) exprNode() {} func (SafeSelectorExpr) exprNode() {}

View file

@ -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},
}
}

View file

@ -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)
}
}

View file

@ -263,6 +263,7 @@ func (g *goGenerator) function(fn FunctionDecl) error {
} }
g.write("func ") g.write("func ")
g.write(fn.Name) g.write(fn.Name)
g.write(renderGoTypeParameters(fn.TypeParams))
g.write(g.renderGoFunctionParams(fn)) g.write(g.renderGoFunctionParams(fn))
if ret := g.goReturnType(fn.ReturnType); ret != "" { if ret := g.goReturnType(fn.ReturnType); ret != "" {
g.write(" ") g.write(" ")
@ -346,7 +347,7 @@ func enumVariant(decl EnumDecl, name string) *EnumVariant {
} }
func (g *goGenerator) classDecl(class ClassDecl) error { func (g *goGenerator) classDecl(class ClassDecl) error {
g.line("type " + class.Name + " struct {") g.line("type " + class.Name + renderGoTypeParameters(class.TypeParams) + " struct {")
g.indentLevel++ g.indentLevel++
for _, field := range class.Fields { for _, field := range class.Fields {
name := field.Name name := field.Name
@ -366,6 +367,7 @@ func (g *goGenerator) classDecl(class ClassDecl) error {
g.line("") g.line("")
g.write("func New") g.write("func New")
g.write(class.Name) g.write(class.Name)
g.write(renderGoTypeParameters(class.TypeParams))
g.write("(") g.write("(")
for i, field := range class.Fields { for i, field := range class.Fields {
if i > 0 { if i > 0 {
@ -377,11 +379,13 @@ func (g *goGenerator) classDecl(class ClassDecl) error {
} }
g.write(") *") g.write(") *")
g.write(class.Name) g.write(class.Name)
g.write(renderGoTypeArguments(class.TypeParams))
g.write(" {\n") g.write(" {\n")
g.indentLevel++ g.indentLevel++
g.writeIndent() g.writeIndent()
g.write("return &") g.write("return &")
g.write(class.Name) g.write(class.Name)
g.write(renderGoTypeArguments(class.TypeParams))
g.write("{") g.write("{")
for i, field := range class.Fields { for i, field := range class.Fields {
if i > 0 { if i > 0 {
@ -427,6 +431,7 @@ func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error {
g.write("func (self *") g.write("func (self *")
g.write(class.Name) g.write(class.Name)
g.write(renderGoTypeArguments(class.TypeParams))
g.write(") ") g.write(") ")
g.write(fn.Name) g.write(fn.Name)
g.write(g.renderGoFunctionParams(fn)) g.write(g.renderGoFunctionParams(fn))
@ -792,8 +797,8 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) {
if e.Name == "this" { if e.Name == "this" {
return "self", nil return "self", nil
} }
if g.classField(*g.currentClass, e.Name) && !g.isDefined(e.Name) { if field, found := classFieldByName(*g.currentClass, e.Name); found && !g.isDefined(e.Name) {
return "self." + e.Name, nil return "self." + mappingFieldName(*g.currentClass, field), nil
} }
} }
return e.Name, 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 ident, ok := e.Callee.(IdentExpr); ok {
if _, ok := g.semantic.Classes[ident.Name]; ok && (resolvedCall == nil || resolvedCall.Meaning == ClassConstructionExpr) { 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 { if ident, ok := e.Callee.(IdentExpr); ok {
@ -1217,6 +1222,9 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
if len(e.TypeArgs) > 0 {
callee += g.renderCallTypeArguments(e.TypeArgs)
}
call := fmt.Sprintf("%s(%s)", callee, strings.Join(args, ", ")) call := fmt.Sprintf("%s(%s)", callee, strings.Join(args, ", "))
if result, ok := g.goErrorResult(e, call, expectedType); ok { if result, ok := g.goErrorResult(e, call, expectedType); ok {
return result, nil return result, nil
@ -2460,18 +2468,11 @@ func (g *goGenerator) isDefined(name string) bool {
return false 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 { func (g *goGenerator) assignTarget(name string) string {
if g.currentClass != nil && g.classField(*g.currentClass, name) && !g.isDefined(name) { if g.currentClass != nil && !g.isDefined(name) {
return "self." + name if field, found := classFieldByName(*g.currentClass, name); found {
return "self." + mappingFieldName(*g.currentClass, field)
}
} }
return name return name
} }
@ -2496,6 +2497,35 @@ func (g *goGenerator) renderGoFunctionParams(function FunctionDecl) string {
return g.renderGoParamsWithPrefix(function.Params, prefix) 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 { func (g *goGenerator) renderGoParamsWithPrefix(params []Param, prefix string) string {
var b strings.Builder var b strings.Builder
b.WriteString("(") b.WriteString("(")

View file

@ -0,0 +1,72 @@
package lang
import (
"strings"
"testing"
)
func TestGenerateGenericFunctionsAndClasses(t *testing.T) {
program, err := Parse(`package demo
data class Box<T>(var value: T) {
fun get(): T { return value }
}
fun identity<T>(value: T): T { return value }
fun wrap<T>(value: T): Box<T> { return Box(value) }
fun main() {
val inferred = identity(42)
val explicit = identity<String>("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<String>" {
t.Fatalf("generic class type = %s", got)
}
}
func TestRejectInvalidGenericDeclarationsAndCalls(t *testing.T) {
for _, test := range []struct{ source, message string }{
{`package demo fun identity<T, T>(value: T): T { return value }`, "duplicate type parameter T"},
{`package demo fun identity<T>(value: T): T { return value } fun main() { identity<String, Int>("x") }`, "expects 1 type arguments, got 2"},
{`package demo class Box<T>(val value: T) { fun convert<R>(): 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)
}
}
}

View file

@ -12,7 +12,7 @@ package demo
import http net.http import http net.http
import json encoding.json import json encoding.json
fun main() { fun main(handler: (http.ResponseWriter, *http.Request) -> Unit, body: ByteSlice, target: Any) {
http.handleFunc("/health", handler) http.handleFunc("/health", handler)
json.unmarshal(body, &target) json.unmarshal(body, &target)
} }

View file

@ -4,14 +4,32 @@ import (
"go/importer" "go/importer"
gotypes "go/types" gotypes "go/types"
"strings" "strings"
"sync"
golangpackages "golang.org/x/tools/go/packages"
) )
var importedGoPackages sync.Map
func importGoPackage(decl ImportDecl) (*gotypes.Package, error) { func importGoPackage(decl ImportDecl) (*gotypes.Package, error) {
path := strings.Trim(decl.Path, `"`) path := strings.Trim(decl.Path, `"`)
if !strings.Contains(path, "/") { if !strings.Contains(path, "/") {
path = importPathToGoPath(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 { func (semantic *SemanticProgram) goSelectorType(alias, name string) Type {
@ -119,7 +137,11 @@ func semanticTypeFromGoResults(results *gotypes.Tuple) Type {
if results.Len() == 1 { if results.Len() == 1 {
return last 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 { func semanticTypeFromGo(typ gotypes.Type) Type {

View file

@ -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<Int, Error>" {
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)
}
}

View file

@ -28,6 +28,7 @@ const (
type ExprMeta struct{ Semantic *HIRExpr } type ExprMeta struct{ Semantic *HIRExpr }
type HIRExpr struct { type HIRExpr struct {
ID int
Type Type Type Type
Meaning ExprMeaning Meaning ExprMeaning
Symbol *Symbol Symbol *Symbol
@ -111,8 +112,9 @@ type HIRFunction struct {
} }
type HIRProgram struct { type HIRProgram struct {
Functions []*HIRFunction Functions []*HIRFunction
Methods []*HIRFunction Methods []*HIRFunction
Expressions []*HIRExpr
} }
func exprMeta(expr Expr) *HIRExpr { func exprMeta(expr Expr) *HIRExpr {
@ -240,10 +242,10 @@ func (resolver *semanticResolver) resolve() error {
symbol, _ := resolver.program.Global.Lookup(decl.Name) symbol, _ := resolver.program.Global.Lookup(decl.Name)
scope := NewScope(resolver.program.Global) scope := NewScope(resolver.program.Global)
for _, param := range decl.Params { 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}) _ = 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.resolveStmts(decl.Body, scope, nil, result)
resolver.program.HIR.Functions = append(resolver.program.HIR.Functions, &HIRFunction{Symbol: symbol, Decl: decl, Scope: scope}) 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 := NewScope(resolver.program.Global)
_ = scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}}) _ = scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}})
for _, param := range method.Params { 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}) _ = 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.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}) 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 { 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[0] = result.Args[0]
valueTypes[1] = NullableType{Element: result.Args[1]} 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 { for index, name := range value.Names {
_ = scope.Define(&Symbol{Name: name, Kind: VariableSymbol, Type: valueTypes[index], Mutable: value.Mutable}) _ = 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{}) expected := Type(UnknownType{})
if symbol, ok := scope.Lookup(value.Name); ok { if symbol, ok := scope.Lookup(value.Name); ok {
expected = symbol.Type 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) value.Value, _ = resolver.resolveExpr(value.Value, scope, class, expected)
stmts[index] = value stmts[index] = value
case AddAssignStmt: 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{}) value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
stmts[index] = value stmts[index] = value
case MultiAssignStmt: 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{}) value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
stmts[index] = value stmts[index] = value
case ReturnStmt: case ReturnStmt:
@ -421,8 +448,11 @@ func (resolver *semanticResolver) resolveExpr(expr Expr, scope *Scope, class *Cl
expr = value expr = value
case LambdaExpr: case LambdaExpr:
lambdaScope := NewScope(scope) lambdaScope := NewScope(scope)
if value.ImplicitIt {
_ = lambdaScope.Define(&Symbol{Name: "it", Kind: VariableSymbol, Type: UnknownType{}})
}
for _, param := range value.Params { 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}) _ = lambdaScope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
} }
resolver.resolveStmts(value.Body, lambdaScope, class, functionResult(expected)) 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) 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.validateEnumExpression(expr, meaning)
resolver.validateGenericCall(expr)
if meaning == GoCallExpr && isResultType(expected) { if meaning == GoCallExpr && isResultType(expected) {
typ = expected typ = expected
} }
if nullable, ok := typ.(NullableType); ok && typeEqual(nullable.Element, expected) { if nullable, ok := typ.(NullableType); ok && typeEqual(nullable.Element, expected) {
typ = 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) semantic.Node = resolver.hirNode(expr, semantic)
resolver.program.HIR.Expressions = append(resolver.program.HIR.Expressions, semantic)
return withExprMeta(expr, semantic), typ 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) { func (resolver *semanticResolver) validateEnumExpression(expr Expr, meaning ExprMeaning) {
if meaning != EnumConstructionExpr { if meaning != EnumConstructionExpr {
return 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 { func collectionElement(typ Type) Type {
if generic, ok := typ.(GenericType); ok && len(generic.Args) > 0 { if generic, ok := typ.(GenericType); ok && len(generic.Args) > 0 {
return generic.Args[len(generic.Args)-1] return generic.Args[len(generic.Args)-1]

View file

@ -17,7 +17,14 @@ func Parse(input string) (*Program, error) {
return nil, err return nil, err
} }
p := &parser{tokens: tokens} 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) { func (p *parser) parseProgram() (*Program, error) {
@ -292,6 +299,10 @@ func (p *parser) parseClass() (ClassDecl, error) {
if err != nil { if err != nil {
return ClassDecl{}, err return ClassDecl{}, err
} }
typeParams, err := p.parseTypeParameters()
if err != nil {
return ClassDecl{}, err
}
var fields []FieldDecl var fields []FieldDecl
if p.match(tokenLParen) { if p.match(tokenLParen) {
fields, err = p.parseClassFields() fields, err = p.parseClassFields()
@ -308,7 +319,7 @@ func (p *parser) parseClass() (ClassDecl, error) {
return ClassDecl{}, err return ClassDecl{}, err
} }
if !p.match(tokenLBrace) { 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 var methods []FunctionDecl
@ -329,7 +340,7 @@ func (p *parser) parseClass() (ClassDecl, error) {
return ClassDecl{}, err 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) { func (p *parser) parseClassParents() ([]string, error) {
@ -467,6 +478,7 @@ func (p *parser) parseFunction() (FunctionDecl, error) {
return FunctionDecl{ return FunctionDecl{
Name: signature.Name, Name: signature.Name,
TypeParams: signature.TypeParams,
Params: signature.Params, Params: signature.Params,
ReturnType: signature.ReturnType, ReturnType: signature.ReturnType,
Body: body, Body: body,
@ -483,6 +495,10 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) {
if err != nil { if err != nil {
return FunctionSignature{}, err return FunctionSignature{}, err
} }
typeParams, err := p.parseTypeParameters()
if err != nil {
return FunctionSignature{}, err
}
if _, err := p.expect(tokenLParen, "expected '('"); err != nil { if _, err := p.expect(tokenLParen, "expected '('"); err != nil {
return FunctionSignature{}, err return FunctionSignature{}, err
} }
@ -505,12 +521,39 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) {
return FunctionSignature{ return FunctionSignature{
Name: name.lexeme, Name: name.lexeme,
TypeParams: typeParams,
Params: params, Params: params,
ReturnType: returnType, ReturnType: returnType,
Suspend: suspend, Suspend: suspend,
}, nil }, 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) { func (p *parser) parseParams() ([]Param, error) {
var params []Param var params []Param
if p.check(tokenRParen) { if p.check(tokenRParen) {
@ -528,7 +571,11 @@ func (p *parser) parseParams() ([]Param, error) {
if err != nil { if err != nil {
return nil, err 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) { if !p.match(tokenComma) {
return params, nil return params, nil
} }
@ -832,7 +879,7 @@ func (p *parser) parseVarDecl(mutable bool) (Stmt, error) {
return nil, err return nil, err
} }
if len(names) == 1 { 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} decl := MultiVarDecl{Mutable: mutable, Names: make([]string, 0, len(names)), Value: value}
for _, name := range names { for _, name := range names {
@ -908,7 +955,7 @@ func (p *parser) parsePrefix() (Expr, error) {
tok := p.advance() tok := p.advance()
switch tok.kind { switch tok.kind {
case tokenIdent: case tokenIdent:
return p.parsePostfix(IdentExpr{Name: tok.lexeme}) return p.parsePostfix(IdentExpr{Name: tok.lexeme, Pos: tok.pos})
case tokenInt: case tokenInt:
return IntExpr{Value: tok.lexeme}, nil return IntExpr{Value: tok.lexeme}, nil
case tokenFloat: case tokenFloat:
@ -999,6 +1046,10 @@ func (p *parser) parseLambdaParams() ([]Param, bool, error) {
return nil, false, err return nil, false, err
} }
param.Type = typ param.Type = typ
param.TypeRef, err = ParseTypeRef(typ)
if err != nil {
return nil, false, err
}
} }
params = append(params, param) params = append(params, param)
if !p.match(tokenComma) { if !p.match(tokenComma) {
@ -1028,13 +1079,13 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) {
if !selectorName(name.lexeme) { if !selectorName(name.lexeme) {
return nil, fmt.Errorf("expected selector name at %d, found %q", name.pos, 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): case p.match(tokenSafeDot):
name := p.advance() name := p.advance()
if !selectorName(name.lexeme) { if !selectorName(name.lexeme) {
return nil, fmt.Errorf("expected selector name at %d, found %q", name.pos, 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): case p.match(tokenDoubleBang):
expr = NonNullExpr{Value: expr} expr = NonNullExpr{Value: expr}
case p.match(tokenQuestion): case p.match(tokenQuestion):

View file

@ -33,7 +33,11 @@ func (checker *mutabilityChecker) checkFunction(function *FunctionDecl, class *C
_ = checker.scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}}) _ = checker.scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}})
} }
for _, param := range function.Params { 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}) _ = checker.scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
} }
return checker.checkStmts(function.Body) return checker.checkStmts(function.Body)
@ -46,7 +50,7 @@ func (checker *mutabilityChecker) checkStmts(statements []Stmt) error {
if err := checker.checkExpr(value.Value); err != nil { if err := checker.checkExpr(value.Value); err != nil {
return err 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}) _ = checker.scope.Define(&Symbol{Name: value.Name, Kind: VariableSymbol, Type: typ, Mutable: value.Mutable})
case MultiVarDecl: case MultiVarDecl:
if err := checker.checkExpr(value.Value); err != nil { 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{}}) _ = checker.scope.Define(&Symbol{Name: "it", Kind: VariableSymbol, Type: UnknownType{}})
} }
for _, param := range value.Params { 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}) _ = checker.scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
} }
return checker.checkStmts(value.Body) return checker.checkStmts(value.Body)

View file

@ -48,26 +48,48 @@ func (scope *Scope) Lookup(name string) (*Symbol, bool) {
} }
type ClassSymbol struct { type ClassSymbol struct {
Name string Name string
Decl *ClassDecl TypeParams []string
Fields map[string]*Symbol Decl *ClassDecl
Methods map[string]*Symbol Fields map[string]*Symbol
Methods map[string]*Symbol
} }
type SemanticProgram struct { type SemanticProgram struct {
Syntax *Program Syntax *Program
Global *Scope Global *Scope
Classes map[string]ClassDecl Classes map[string]ClassDecl
ClassInfo map[string]*ClassSymbol ClassInfo map[string]*ClassSymbol
Functions map[string]FunctionDecl Functions map[string]FunctionDecl
Enums map[string]EnumDecl Enums map[string]EnumDecl
Imports map[string]bool Imports map[string]bool
GoPackages map[string]*gotypes.Package GoPackages map[string]*gotypes.Package
HIR *HIRProgram HIR *HIRProgram
Mappings *mappingState Mappings *mappingState
Diagnostics []SemanticDiagnostic
} }
func Analyze(program *Program) (*SemanticProgram, error) { 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{ semantic := &SemanticProgram{
Syntax: program, Syntax: program,
Global: NewScope(nil), Global: NewScope(nil),
@ -79,67 +101,94 @@ func Analyze(program *Program) (*SemanticProgram, error) {
GoPackages: map[string]*gotypes.Package{}, GoPackages: map[string]*gotypes.Package{},
Mappings: &mappingState{functions: map[string]string{}}, Mappings: &mappingState{functions: map[string]string{}},
} }
for index := range program.Classes { programs := append([]*Program{program}, additional...)
decl := &program.Classes[index] for _, source := range programs {
class := &ClassSymbol{Name: decl.Name, Decl: decl, Fields: map[string]*Symbol{}, Methods: map[string]*Symbol{}} for index := range source.Classes {
semantic.Classes[decl.Name] = *decl decl := &source.Classes[index]
semantic.ClassInfo[decl.Name] = class class := &ClassSymbol{Name: decl.Name, TypeParams: decl.TypeParams, Decl: decl, Fields: map[string]*Symbol{}, Methods: map[string]*Symbol{}}
if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: ClassSymbolKind, Type: ClassType{Class: class}, Decl: decl}); err != nil { semantic.Classes[decl.Name] = *decl
return nil, err 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)
} }
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] for _, source := range programs {
typ, err := semantic.functionType(method.Params, method.ReturnType) for index := range source.Enums {
if err != nil { decl := &source.Enums[index]
return nil, fmt.Errorf("method %s.%s: %w", decl.Name, method.Name, err) 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 { if err := validateMutability(semantic); err != nil {
@ -164,6 +213,30 @@ func (semantic *SemanticProgram) ResolveType(text string) (Type, error) {
return resolved, nil 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 { func validateNoClassPointer(typ Type) error {
switch value := typ.(type) { switch value := typ.(type) {
case GoPointerType: case GoPointerType:
@ -190,20 +263,24 @@ func validateNoClassPointer(typ Type) error {
return nil 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)) paramTypes := make([]Type, len(params))
for index, param := range params { for index, param := range params {
typ, err := semantic.ResolveType(param.Type) typ, err := semantic.ResolveTypeRefWithParams(param.TypeRef, typeParams)
if err != nil { if err != nil {
return nil, err return nil, err
} }
paramTypes[index] = typ paramTypes[index] = typ
} }
resultType, err := semantic.ResolveType(result) resultRef, err := ParseTypeRef(result)
if err != nil { if err != nil {
return nil, err 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 { func (semantic *SemanticProgram) GoType(text string) string {

View file

@ -66,6 +66,14 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment)
return resolve(value.TypeArgs[0]) return resolve(value.TypeArgs[0])
} }
receiverType := semantic.TypeOf(selector.Receiver, environment) 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 { if iterator, ok := receiverType.(GenericType); ok && iterator.Base.String() == "GotlinSQLIterator" && len(iterator.Args) == 1 {
switch selector.Name { switch selector.Name {
case "next": case "next":
@ -78,10 +86,10 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment)
return NamedType{Name: "Unit"} 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 method, ok := class.Methods[selector.Name]; ok {
if function, ok := method.Type.(FunctionType); 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 { 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 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 { 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]}} return GenericType{Base: NamedType{Name: "List"}, Args: []Type{mapping.Args[0]}}
} }
} }
if function, ok := semantic.Functions[ident.Name]; ok { if symbol, ok := semantic.Global.Lookup(ident.Name); ok && symbol.Kind == FunctionSymbolKind {
return resolve(function.ReturnType) 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 { 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 { if sqlType, ok := sqlChainResultType(value); ok {
@ -121,9 +175,9 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment)
return NamedType{Name: receiver.Name} 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 { 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) { 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 { func classTypeOf(typ Type) *ClassSymbol {
class, _ := classInstance(typ)
return class
}
func classInstance(typ Type) (*ClassSymbol, map[string]Type) {
switch value := typ.(type) { switch value := typ.(type) {
case ClassType: case ClassType:
return value.Class return value.Class, map[string]Type{}
case NullableType: 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 { func nullableSemanticType(typ Type) Type {

248
internal/lang/type_refs.go Normal file
View file

@ -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
}

View file

@ -10,6 +10,19 @@ type Type interface {
String() string 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{} type UnknownType struct{}
func (UnknownType) typeNode() {} func (UnknownType) typeNode() {}
@ -20,6 +33,11 @@ type NamedType struct{ Name string }
func (NamedType) typeNode() {} func (NamedType) typeNode() {}
func (t NamedType) String() string { return t.Name } 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 } type ClassType struct{ Class *ClassSymbol }
func (ClassType) typeNode() {} func (ClassType) typeNode() {}
@ -36,8 +54,9 @@ func (GoPointerType) typeNode() {}
func (t GoPointerType) String() string { return "*" + t.Element.String() } func (t GoPointerType) String() string { return "*" + t.Element.String() }
type FunctionType struct { type FunctionType struct {
Params []Type TypeParams []string
Result Type Params []Type
Result Type
} }
func (FunctionType) typeNode() {} func (FunctionType) typeNode() {}
@ -54,6 +73,17 @@ type GenericType struct {
Args []Type 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 (GenericType) typeNode() {}
func (t GenericType) String() string { func (t GenericType) String() string {
args := make([]string, 0, len(t.Args)) 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 ClassType{Class: class}
} }
return value return value
case TypeParameterType:
return value
case NullableType: case NullableType:
return NullableType{Element: resolveClassTypes(value.Element, classes)} return NullableType{Element: resolveClassTypes(value.Element, classes)}
case GoPointerType: case GoPointerType:
@ -141,6 +173,46 @@ func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type {
args[i] = resolveClassTypes(arg, classes) args[i] = resolveClassTypes(arg, classes)
} }
return GenericType{Base: resolveClassTypes(value.Base, classes), Args: args} 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: default:
return typ 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 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 { func renderGoType(typ Type) string {
switch value := typ.(type) { switch value := typ.(type) {
case UnknownType: case UnknownType:
@ -201,7 +330,12 @@ func renderGoType(typ Type) string {
} }
return "*GotlinSQLIterator[" + argument + "]" return "*GotlinSQLIterator[" + argument + "]"
} }
if class, ok := value.Base.(ClassType); ok {
return "*" + class.Class.Name + "[" + strings.Join(args, ", ") + "]"
}
return base + "[" + strings.Join(args, ", ") + "]" return base + "[" + strings.Join(args, ", ") + "]"
case TypeParameterType:
return value.Name
case NamedType: case NamedType:
switch value.Name { switch value.Name {
case "Int": case "Int":
@ -225,6 +359,12 @@ func renderGoType(typ Type) string {
default: default:
return value.Name 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: default:
return "" return ""
} }

View file

@ -35,6 +35,9 @@ fun create(): Service { return Service(Repository()) }`)
if program.Classes[1].Fields[0].Type != "Repository" || program.Functions[0].ReturnType != "Service" { 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]) 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 { if _, err := GenerateGo(program); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -67,6 +70,9 @@ fun parse(value: String): Result<Int, Error> {
if semantic.HIR == nil || len(semantic.HIR.Functions) != 1 { if semantic.HIR == nil || len(semantic.HIR.Functions) != 1 {
t.Fatal("typed HIR function was not created") 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) decl := program.Functions[0].Body[0].(VarDecl)
attempt := decl.Value.(TryExpr) attempt := decl.Value.(TryExpr)
if attempt.Meta.Semantic == nil || attempt.Meta.Semantic.Meaning != PropagateResultExpr || attempt.Meta.Semantic.Type.String() != "Int" { 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<Int, Error> {
if _, ok := attempt.Meta.Semantic.Node.(HIRPropagateResult); !ok { if _, ok := attempt.Meta.Semantic.Node.(HIRPropagateResult); !ok {
t.Fatalf("propagation did not lower to HIRPropagateResult: %#v", attempt.Meta.Semantic.Node) 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) call := attempt.Value.(CallExpr)
if call.Meta.Semantic == nil || call.Meta.Semantic.Meaning != GoCallExpr || call.Meta.Semantic.Type.String() != "Result<Int, Error>" { if call.Meta.Semantic == nil || call.Meta.Semantic.Meaning != GoCallExpr || call.Meta.Semantic.Type.String() != "Result<Int, Error>" {
t.Fatalf("external call was not resolved: %#v", call.Meta.Semantic) t.Fatalf("external call was not resolved: %#v", call.Meta.Semantic)

View file

@ -82,7 +82,7 @@ for (const declaration of [
const prefixes = new Set(Object.values(snippets).map((snippet) => snippet.prefix)); const prefixes = new Set(Object.values(snippets).map((snippet) => snippet.prefix));
for (const prefix of [ 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" "sqlsingle", "sqliterator", "sqlinsertnothing", "sqlinsertupdate", "sqlupdatereturning"
]) { ]) {
assert(prefixes.has(prefix), `snippets are missing prefix ${prefix}`); assert(prefixes.has(prefix), `snippets are missing prefix ${prefix}`);

View file

@ -36,6 +36,22 @@
], ],
"description": "Gotlin data class" "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": { "SQL Table Row": {
"prefix": "tablerow", "prefix": "tablerow",
"body": [ "body": [