commit 8f4f858d8600ee7833fb9aa6560e53c14caed350 Author: Pavel Flegr Date: Thu Mar 5 14:55:09 2026 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc04bec --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +.gocache +bin +out \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d9ee57 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# Gotlin + +`Gotlin` is a small Kotlin-like frontend implemented in Go that targets the Go toolchain. + +This is the practical boundary of the prototype: + +- It is a Kotlin-flavored language frontend. +- It targets the Go toolchain by generating valid Go source and building through `go build`. +- It is not a direct integration into Go's internal `cmd/compile` backend APIs. + +## Supported language slice + +- `fun` declarations +- `val` and `var` +- `Int`, `String`, `Boolean`, `Unit` +- function types like `(String) -> Unit` +- `if`, `else`, `while` +- function calls +- lambdas like `{ x: Int -> println(x) }` and `{ println(it) }` +- `class` with primary-constructor fields and methods +- `interface` with method signatures +- `println(...)` +- arithmetic, comparison, and boolean operators + +## Example + +```kotlin +package demo + +fun fib(n: Int): Int { + if (n < 2) { + return n + } + return fib(n - 1) + fib(n - 2) +} + +fun main() { + println(fib(8)) +} +``` + +Imports from Go packages are supported: + +```kotlin +package demo + +import strings + +fun main() { + println(strings.ToUpper("gotlin")) +} +``` + +HTTP server example: + +```kotlin +package demo.web + +import fmt +import net.http + +fun helloHandler(w: http.ResponseWriter, r: *http.Request) { + fmt.Fprintln(w, "hello from gotlin") +} + +fun main() { + http.HandleFunc("/", helloHandler) + fmt.Println("serving http://localhost:8080") + http.ListenAndServe(":8080", http.DefaultServeMux) +} +``` + +Classes and interfaces: + +```kotlin +package demo + +interface Greeter { + fun greet(name: String): String +} + +class ConsoleGreeter(val prefix: String) { + fun greet(name: String): String { + return prefix + name + } +} + +fun main() { + val greeter: Greeter = ConsoleGreeter("hello, ") + println(greeter.greet("gotlin")) +} +``` + +```bash +go run ./cmd/gotlinc build ./examples/hello.gt +./hello +``` + +Emit Go source instead: + +```bash +go run ./cmd/gotlinc build -src ./examples/hello.gt -o /tmp/hello.go +go run /tmp/hello.go +``` + +Run directly: + +```bash +go run ./cmd/gotlinc run ./examples/hello.gt +``` + +Language server: + +```bash +go build -o ./bin/gotlin-lsp ./cmd/gotlin-lsp +./bin/gotlin-lsp +``` + +VS Code extension: + +```bash +cd ./tools/vscode-gotlin +npm install +npm run build +``` + +## Notes + +- `val` and `var` currently compile to the same local-variable semantics in Go. +- Type inference is local to declarations without an explicit type. +- Top-level declarations currently support functions, classes, and interfaces. +- `gotlinc build` produces an executable by default. If `-o` is omitted, the output name is derived from the input file name. +- `gotlinc build -src` emits Go source instead of a binary. +- `gotlinc` supports `build` and `run`, and defaults to `build` if no subcommand is given. +- Gotlin source files use the `.gt` extension. +- `gotlin-lsp` provides diagnostics, hover, and go-to-definition over stdio. +- `gotlin-lsp` can optionally use `gopls` for hover and definition on Go-imported symbols. +- the VS Code extension adds syntax highlighting, snippets, and launches the LSP for `.gt` files. diff --git a/cmd/gotlin-lsp/README.md b/cmd/gotlin-lsp/README.md new file mode 100644 index 0000000..50205f0 --- /dev/null +++ b/cmd/gotlin-lsp/README.md @@ -0,0 +1,39 @@ +# gotlin-lsp + +Minimal stdio language server for `.gt` files. + +Current support: + +- `initialize` +- `shutdown` +- `exit` +- `textDocument/didOpen` +- `textDocument/didChange` +- `textDocument/didClose` +- publish diagnostics from the existing Gotlin parser/code generator +- `textDocument/hover` for packages, imports, and top-level functions +- `textDocument/definition` for imported namespaces and top-level functions +- simple semantic diagnostics for duplicate imports/functions and undefined names +- optional fallback to `gopls` for hover/definition on Go-imported symbols + +Build: + +```bash +go build -o ./bin/gotlin-lsp ./cmd/gotlin-lsp +``` + +Use your editor's custom LSP configuration to launch: + +```bash +./bin/gotlin-lsp +``` + +## gopls bridge + +If `gopls` is installed, `gotlin-lsp` will try to use it as a fallback for hover and definition on Go-imported symbols. + +You can set an explicit path with: + +```bash +GOTLIN_GOPLS_PATH=/absolute/path/to/gopls ./bin/gotlin-lsp +``` diff --git a/cmd/gotlin-lsp/main.go b/cmd/gotlin-lsp/main.go new file mode 100644 index 0000000..0f42a61 --- /dev/null +++ b/cmd/gotlin-lsp/main.go @@ -0,0 +1,2293 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "sort" + "strconv" + "strings" + "unicode" + + "gotlin/internal/lang" +) + +const ( + textDocumentSyncFull = 1 + diagnosticSeverityError = 1 + diagnosticSeverityWarning = 2 + symbolKindPackage = 4 + symbolKindClass = 5 + symbolKindMethod = 6 + symbolKindField = 8 + symbolKindVariable = 13 + symbolKindFunction = 12 + symbolKindInterface = 11 + symbolKindModule = 2 +) + +var offsetPattern = regexp.MustCompile(` at (\d+)`) + +func main() { + server := server{ + in: bufio.NewReader(os.Stdin), + out: os.Stdout, + docs: map[string]documentState{}, + goplsPath: resolveGoplsPath(), + } + if err := server.run(); err != nil && !errors.Is(err, io.EOF) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +type server struct { + in *bufio.Reader + out io.Writer + docs map[string]documentState + shutdown bool + goplsPath string +} + +type documentState struct { + text string + program *lang.Program + diagnostics []diagnostic + symbols []symbol +} + +type symbol struct { + Name string + Kind int + Detail string + Range rng + Targets []string +} + +type variableDeclInfo struct { + Name string + Mutable bool + Type string +} + +type request struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type response struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id,omitempty"` + Result any `json:"result"` + Error *respError `json:"error,omitempty"` +} + +type respError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type notification struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params any `json:"params,omitempty"` +} + +type didOpenParams struct { + TextDocument textDocumentItem `json:"textDocument"` +} + +type didChangeParams struct { + TextDocument versionedTextDocumentIdentifier `json:"textDocument"` + ContentChanges []contentChange `json:"contentChanges"` +} + +type didCloseParams struct { + TextDocument textDocumentIdentifier `json:"textDocument"` +} + +type hoverParams struct { + TextDocument textDocumentIdentifier `json:"textDocument"` + Position position `json:"position"` +} + +type definitionParams struct { + TextDocument textDocumentIdentifier `json:"textDocument"` + Position position `json:"position"` +} + +type textDocumentItem struct { + URI string `json:"uri"` + Text string `json:"text"` +} + +type versionedTextDocumentIdentifier struct { + URI string `json:"uri"` +} + +type textDocumentIdentifier struct { + URI string `json:"uri"` +} + +type contentChange struct { + Text string `json:"text"` +} + +type publishDiagnosticsParams struct { + URI string `json:"uri"` + Diagnostics []diagnostic `json:"diagnostics"` +} + +type diagnostic struct { + Range rng `json:"range"` + Severity int `json:"severity,omitempty"` + Source string `json:"source,omitempty"` + Message string `json:"message"` +} + +type rng struct { + Start position `json:"start"` + End position `json:"end"` +} + +type position struct { + Line int `json:"line"` + Character int `json:"character"` +} + +type location struct { + URI string `json:"uri"` + Range rng `json:"range"` +} + +type stdlibTarget struct { + PackagePath string + SymbolName string +} + +type stdlibSymbol struct { + FileName string + Start position + End position + Decl string + Doc string +} + +func (s *server) run() error { + for { + msg, err := readMessage(s.in) + if err != nil { + return err + } + + var req request + if err := json.Unmarshal(msg, &req); err != nil { + return err + } + + if err := s.handle(req); err != nil { + return err + } + if s.shutdown && req.Method == "exit" { + return nil + } + } +} + +func (s *server) handle(req request) error { + switch req.Method { + case "initialize": + return s.writeResponse(response{ + JSONRPC: "2.0", + ID: req.ID, + Result: map[string]any{ + "capabilities": map[string]any{ + "textDocumentSync": textDocumentSyncFull, + "hoverProvider": true, + "definitionProvider": true, + }, + "serverInfo": map[string]any{ + "name": "gotlin-lsp", + "version": "0.2.0", + }, + }, + }) + case "initialized": + return nil + case "shutdown": + s.shutdown = true + return s.writeResponse(response{JSONRPC: "2.0", ID: req.ID, Result: nil}) + case "exit": + return nil + case "textDocument/didOpen": + var params didOpenParams + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return err + } + s.docs[params.TextDocument.URI] = buildDocumentState(params.TextDocument.Text) + return s.publishDiagnostics(params.TextDocument.URI) + case "textDocument/didChange": + var params didChangeParams + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return err + } + text := "" + if len(params.ContentChanges) > 0 { + text = params.ContentChanges[len(params.ContentChanges)-1].Text + } + s.docs[params.TextDocument.URI] = buildDocumentState(text) + return s.publishDiagnostics(params.TextDocument.URI) + case "textDocument/didClose": + var params didCloseParams + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return err + } + delete(s.docs, params.TextDocument.URI) + return s.writeNotification(notification{ + JSONRPC: "2.0", + Method: "textDocument/publishDiagnostics", + Params: publishDiagnosticsParams{URI: params.TextDocument.URI, Diagnostics: []diagnostic{}}, + }) + case "textDocument/hover": + var params hoverParams + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return err + } + return s.writeResponse(response{ + JSONRPC: "2.0", + ID: req.ID, + Result: s.hover(params.TextDocument.URI, params.Position), + }) + case "textDocument/definition": + var params definitionParams + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return err + } + return s.writeResponse(response{ + JSONRPC: "2.0", + ID: req.ID, + Result: s.definition(params.TextDocument.URI, params.Position), + }) + default: + if len(req.ID) == 0 { + return nil + } + return s.writeResponse(response{ + JSONRPC: "2.0", + ID: req.ID, + Error: &respError{Code: -32601, Message: "method not found"}, + }) + } +} + +func buildDocumentState(text string) documentState { + state := documentState{text: text, diagnostics: []diagnostic{}} + program, err := lang.Parse(text) + if err != nil { + state.diagnostics = []diagnostic{diagnosticFromError(text, err)} + return state + } + state.program = program + state.symbols = indexSymbols(text, program) + state.diagnostics = semanticDiagnostics(text, program, state.symbols) + if _, err := lang.GenerateGo(program); err != nil { + state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err)) + } + return state +} + +func (s *server) publishDiagnostics(uri string) error { + state, ok := s.docs[uri] + if !ok { + return nil + } + diagnostics := state.diagnostics + if diagnostics == nil { + diagnostics = []diagnostic{} + } + return s.writeNotification(notification{ + JSONRPC: "2.0", + Method: "textDocument/publishDiagnostics", + Params: publishDiagnosticsParams{ + URI: uri, + Diagnostics: diagnostics, + }, + }) +} + +func (s *server) hover(uri string, pos position) any { + state, ok := s.docs[uri] + if !ok { + return nil + } + word, _ := wordAtPosition(state.text, pos) + if word == "" { + return nil + } + if result := stdlibHover(state, pos); result != nil { + return result + } + for _, sym := range state.symbols { + if sym.Name != word { + continue + } + if !rangeContains(sym.Range, pos) && !contains(sym.Targets, word) { + // allow hover on usages by name match + } + return map[string]any{ + "contents": map[string]any{ + "kind": "markdown", + "value": "```gotlin\n" + sym.Detail + "\n```", + }, + } + } + if value, ok := builtinHoverDetail(word); ok { + return map[string]any{ + "contents": map[string]any{ + "kind": "markdown", + "value": "```gotlin\n" + value + "\n```", + }, + } + } + if result := s.goplsHover(state, pos); result != nil { + return result + } + return nil +} + +func (s *server) definition(uri string, pos position) any { + state, ok := s.docs[uri] + if !ok { + return nil + } + word, _ := wordAtPosition(state.text, pos) + if word == "" { + return nil + } + if result := stdlibDefinition(state, pos); result != nil { + return result + } + for _, sym := range state.symbols { + if sym.Name == word { + return []location{{URI: uri, Range: sym.Range}} + } + } + if result := s.goplsDefinition(state, pos); result != nil { + return result + } + return nil +} + +func (s *server) goplsHover(state documentState, pos position) any { + if s.goplsPath == "" || state.program == nil { + return nil + } + query, tokenStart, ok := goplsQueryAtPosition(state.text, pos) + if !ok { + return nil + } + source, targetOffset, cleanup, err := prepareGoplsSource(state.program, state.text, query, tokenStart) + if err != nil { + return nil + } + defer cleanup() + + output, err := exec.Command(s.goplsPath, "hover", fmt.Sprintf("%s:#%d", source, targetOffset)).CombinedOutput() + if err != nil || len(bytes.TrimSpace(output)) == 0 { + return nil + } + return map[string]any{ + "contents": map[string]any{ + "kind": "markdown", + "value": "```go\n" + strings.TrimSpace(string(output)) + "\n```", + }, + } +} + +func stdlibHover(state documentState, pos position) any { + target, ok := resolveStdlibTarget(state, pos) + if !ok { + return nil + } + sym, ok := findStdlibSymbol(target) + if !ok { + return nil + } + var value strings.Builder + value.WriteString("```go\n") + value.WriteString(sym.Decl) + value.WriteString("\n```") + if sym.Doc != "" { + value.WriteString("\n") + value.WriteString(sym.Doc) + } + return map[string]any{ + "contents": map[string]any{ + "kind": "markdown", + "value": value.String(), + }, + } +} + +func stdlibDefinition(state documentState, pos position) any { + target, ok := resolveStdlibTarget(state, pos) + if !ok { + return nil + } + sym, ok := findStdlibSymbol(target) + if !ok { + return nil + } + return []location{{ + URI: "file://" + filepath.Clean(sym.FileName), + Range: rng{ + Start: sym.Start, + End: sym.End, + }, + }} +} + +func (s *server) goplsDefinition(state documentState, pos position) any { + if s.goplsPath == "" || state.program == nil { + return nil + } + query, tokenStart, ok := goplsQueryAtPosition(state.text, pos) + if !ok { + return nil + } + source, targetOffset, cleanup, err := prepareGoplsSource(state.program, state.text, query, tokenStart) + if err != nil { + return nil + } + tempDir := filepath.Dir(source) + defer cleanup() + + output, err := exec.Command(s.goplsPath, "definition", fmt.Sprintf("%s:#%d", source, targetOffset)).CombinedOutput() + if err != nil || len(bytes.TrimSpace(output)) == 0 { + return nil + } + loc, ok := parseGoplsDefinition(string(output)) + if !ok { + return nil + } + if isTempGoplsLocation(loc, tempDir) { + return nil + } + return []location{loc} +} + +func indexSymbols(text string, program *lang.Program) []symbol { + var symbols []symbol + lines := strings.Split(text, "\n") + + if program.PackagePath != "" { + if r, ok := findLineMatch(lines, regexp.MustCompile(`^\s*package\s+([A-Za-z_][\w.-]*(?:\.[A-Za-z_][\w.-]*)*)`), 1); ok { + symbols = append(symbols, symbol{ + Name: lastPackageSegment(program.PackagePath), + Kind: symbolKindPackage, + Detail: "package " + program.PackagePath, + Range: r, + }) + } + } + + importRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*import\s+((?:[A-Za-z_][\w-]*\.)*[A-Za-z_][\w-]*)(?:\s+((?:[A-Za-z_][\w-]*\.)*[A-Za-z_][\w-]*))?`)) + for i, imp := range program.Imports { + name := importAlias(imp) + if i < len(importRanges) { + symbols = append(symbols, symbol{ + Name: name, + Kind: symbolKindModule, + Detail: renderImportDetail(imp), + Range: importRanges[i], + }) + } + } + + interfaceRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*interface\s+([A-Za-z_][A-Za-z0-9_]*)`)) + for i, decl := range program.Interfaces { + r := rng{} + if i < len(interfaceRanges) { + r = interfaceRanges[i] + } + symbols = append(symbols, symbol{ + Name: decl.Name, + Kind: symbolKindInterface, + Detail: renderInterfaceSignature(decl), + Range: r, + }) + for _, method := range decl.Methods { + symbols = append(symbols, symbol{ + Name: method.Name, + Kind: symbolKindMethod, + Detail: renderFunctionSignatureFromParts(method.Name, method.Params, method.ReturnType), + Range: r, + Targets: []string{decl.Name}, + }) + } + } + + classRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*class\s+([A-Za-z_][A-Za-z0-9_]*)`)) + for i, decl := range program.Classes { + r := rng{} + if i < len(classRanges) { + r = classRanges[i] + } + symbols = append(symbols, symbol{ + Name: decl.Name, + Kind: symbolKindClass, + Detail: renderClassSignature(decl), + Range: r, + }) + for _, field := range decl.Fields { + symbols = append(symbols, symbol{ + Name: field.Name, + Kind: symbolKindField, + Detail: renderFieldSignature(decl.Name, field), + Range: r, + Targets: []string{decl.Name}, + }) + } + for _, method := range decl.Methods { + symbols = append(symbols, symbol{ + Name: method.Name, + Kind: symbolKindMethod, + Detail: renderMethodSignature(decl.Name, method), + Range: r, + Targets: []string{decl.Name}, + }) + } + } + workerRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*worker\s+([A-Za-z_][A-Za-z0-9_]*)`)) + for i, decl := range program.Workers { + r := rng{} + if i < len(workerRanges) { + r = workerRanges[i] + } + symbols = append(symbols, symbol{ + Name: decl.Name, + Kind: symbolKindClass, + Detail: renderWorkerSignature(decl), + Range: r, + }) + for _, field := range decl.Fields { + symbols = append(symbols, symbol{ + Name: field.Name, + Kind: symbolKindField, + Detail: renderWorkerFieldSignature(decl.Name, field), + Range: r, + Targets: []string{decl.Name}, + }) + } + for _, method := range decl.Methods { + symbols = append(symbols, symbol{ + Name: method.Name, + Kind: symbolKindMethod, + Detail: renderMethodSignature(decl.Name, method), + Range: r, + Targets: []string{decl.Name}, + }) + } + } + + funcRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*fun\s+([A-Za-z_][A-Za-z0-9_]*)`)) + for i, fn := range program.Functions { + detail := renderFunctionSignature(fn) + r := rng{} + if i < len(funcRanges) { + r = funcRanges[i] + } + symbols = append(symbols, symbol{ + Name: fn.Name, + Kind: symbolKindFunction, + Detail: detail, + Range: r, + }) + } + + varRanges := findAllLineNamedMatches(lines, regexp.MustCompile(`^\s*(?:val|var)\s+([A-Za-z_][A-Za-z0-9_]*)`), 1) + varInfos := collectVariableDeclInfos(program) + varInfoByName := map[string][]variableDeclInfo{} + for _, info := range varInfos { + varInfoByName[info.Name] = append(varInfoByName[info.Name], info) + } + for i, decl := range varRanges { + mutable := false + typ := "" + if queue := varInfoByName[decl.Name]; len(queue) > 0 { + match := queue[0] + varInfoByName[decl.Name] = queue[1:] + mutable = match.Mutable + typ = match.Type + } else if i < len(varInfos) { + // Fallback for parser/regex mismatches. + mutable = varInfos[i].Mutable + typ = varInfos[i].Type + } + symbols = append(symbols, symbol{ + Name: decl.Name, + Kind: symbolKindVariable, + Detail: renderVariableSignature(decl.Name, mutable, typ), + Range: decl.Range, + }) + } + + return symbols +} + +func semanticDiagnostics(text string, program *lang.Program, symbols []symbol) []diagnostic { + var diagnostics []diagnostic + + funcSymbols := map[string]symbol{} + importSymbols := map[string]symbol{} + typeSymbols := map[string]symbol{} + for _, sym := range symbols { + switch sym.Kind { + case symbolKindFunction: + if prev, ok := funcSymbols[sym.Name]; ok { + diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate function "+sym.Name)) + diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate function "+sym.Name)) + } else { + funcSymbols[sym.Name] = sym + } + case symbolKindModule: + if prev, ok := importSymbols[sym.Name]; ok { + diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate import alias "+sym.Name)) + diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate import alias "+sym.Name)) + } else { + importSymbols[sym.Name] = sym + } + case symbolKindClass, symbolKindInterface: + 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 + } + } + } + + 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.Workers { + 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)...) + } + } + for _, worker := range program.Workers { + fields := map[string]bool{ + "this": true, + } + for _, field := range worker.Fields { + fields[field.Name] = true + } + for _, method := range worker.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.GoStmt: + 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.SelectStmt: + for _, c := range s.Cases { + walkExpr(c.Source, scope) + caseScope := copyScope(scope) + caseScope["it"] = true + walkStmts(c.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: + if ident, ok := e.Callee.(lang.IdentExpr); ok { + switch ident.Name { + case "after", "every": + if len(e.Args) != 1 { + diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects exactly one Int argument")) + } + if len(e.Args) == 1 { + switch e.Args[0].(type) { + case lang.StringExpr, lang.BoolExpr, lang.NullExpr: + diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects an Int argument")) + } + } + if ident.Name == "every" && len(e.Args) == 1 { + if value, ok := staticIntExprValue(e.Args[0]); ok && value <= 0 { + diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, "every(ms) requires ms > 0")) + } + } + } + } + walkExpr(e.Callee, scope) + for _, arg := range e.Args { + walkExpr(arg, scope) + } + case lang.SelectorExpr: + walkExpr(e.Receiver, 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 +} + +func diagnosticFromError(text string, err error) diagnostic { + start := position{} + end := position{} + + if match := offsetPattern.FindStringSubmatch(err.Error()); len(match) == 2 { + if offset, convErr := strconv.Atoi(match[1]); convErr == nil { + start = offsetToPosition(text, offset) + end = start + end.Character++ + } + } + + return diagnostic{ + Range: rng{Start: start, End: end}, + Severity: diagnosticSeverityError, + Source: "gotlin", + Message: err.Error(), + } +} + +func duplicateDiagnostic(r rng, message string) diagnostic { + return diagnostic{ + Range: r, + Severity: diagnosticSeverityWarning, + Source: "gotlin", + Message: message, + } +} + +func undefinedNameDiagnostic(text string, name string, message string) diagnostic { + r := findWordRange(text, name) + return diagnostic{ + Range: r, + Severity: diagnosticSeverityWarning, + Source: "gotlin", + Message: message, + } +} + +func offsetToPosition(text string, offset int) position { + runes := []rune(text) + if offset < 0 { + offset = 0 + } + if offset > len(runes) { + offset = len(runes) + } + line := 0 + char := 0 + for i := 0; i < offset; i++ { + if runes[i] == '\n' { + line++ + char = 0 + continue + } + char++ + } + return position{Line: line, Character: char} +} + +func positionToOffset(text string, pos position) int { + runes := []rune(text) + line := 0 + char := 0 + for i, r := range runes { + if line == pos.Line && char == pos.Character { + return i + } + if r == '\n' { + line++ + char = 0 + if line > pos.Line { + return i + } + continue + } + char++ + } + return len(runes) +} + +func wordAtPosition(text string, pos position) (string, rng) { + runes := []rune(text) + offset := positionToOffset(text, pos) + if len(runes) == 0 { + return "", rng{} + } + if offset >= len(runes) { + offset = len(runes) - 1 + } + if !isWordRune(runes[offset]) && offset > 0 && isWordRune(runes[offset-1]) { + offset-- + } + if !isWordRune(runes[offset]) { + return "", rng{} + } + start := offset + for start > 0 && isWordRune(runes[start-1]) { + start-- + } + end := offset + for end+1 < len(runes) && isWordRune(runes[end+1]) { + end++ + } + return string(runes[start : end+1]), rng{ + Start: offsetToPosition(text, start), + End: offsetToPosition(text, end+1), + } +} + +func findWordRange(text string, word string) rng { + runes := []rune(text) + target := []rune(word) + for i := 0; i+len(target) <= len(runes); i++ { + if string(runes[i:i+len(target)]) != word { + continue + } + beforeOk := i == 0 || !isWordRune(runes[i-1]) + afterOk := i+len(target) == len(runes) || !isWordRune(runes[i+len(target)]) + if beforeOk && afterOk { + return rng{ + Start: offsetToPosition(text, i), + End: offsetToPosition(text, i+len(target)), + } + } + } + return rng{} +} + +func isWordRune(r rune) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' +} + +func readMessage(r *bufio.Reader) ([]byte, error) { + contentLength := -1 + for { + line, err := r.ReadString('\n') + if err != nil { + return nil, err + } + line = strings.TrimRight(line, "\r\n") + if line == "" { + break + } + if strings.HasPrefix(strings.ToLower(line), "content-length:") { + value := strings.TrimSpace(line[len("content-length:"):]) + n, err := strconv.Atoi(value) + if err != nil { + return nil, err + } + contentLength = n + } + } + if contentLength < 0 { + return nil, fmt.Errorf("missing Content-Length header") + } + body := make([]byte, contentLength) + if _, err := io.ReadFull(r, body); err != nil { + return nil, err + } + return body, nil +} + +func (s *server) writeResponse(resp response) error { + data, err := json.Marshal(resp) + if err != nil { + return err + } + return writeMessage(s.out, data) +} + +func (s *server) writeNotification(note notification) error { + data, err := json.Marshal(note) + if err != nil { + return err + } + return writeMessage(s.out, data) +} + +func writeMessage(w io.Writer, payload []byte) error { + var buf bytes.Buffer + fmt.Fprintf(&buf, "Content-Length: %d\r\n\r\n", len(payload)) + buf.Write(payload) + _, err := w.Write(buf.Bytes()) + return err +} + +func findLineMatch(lines []string, pattern *regexp.Regexp, capture int) (rng, bool) { + matches := findAllLineMatches(lines, pattern) + if capture == 1 && len(matches) > 0 { + return matches[0], true + } + return rng{}, false +} + +func findAllLineMatches(lines []string, pattern *regexp.Regexp) []rng { + var out []rng + for i, line := range lines { + idx := pattern.FindStringSubmatchIndex(line) + if idx == nil || len(idx) < 4 { + continue + } + start := idx[2] + end := idx[3] + out = append(out, rng{ + Start: position{Line: i, Character: utf16Len(line[:start])}, + End: position{Line: i, Character: utf16Len(line[:end])}, + }) + } + return out +} + +type namedRange struct { + Name string + Range rng +} + +func findAllLineNamedMatches(lines []string, pattern *regexp.Regexp, capture int) []namedRange { + var out []namedRange + for i, line := range lines { + matches := pattern.FindAllStringSubmatchIndex(line, -1) + for _, idx := range matches { + cStart := capture * 2 + cEnd := cStart + 1 + if cEnd >= len(idx) || idx[cStart] < 0 || idx[cEnd] < 0 { + continue + } + start := idx[cStart] + end := idx[cEnd] + name := line[start:end] + out = append(out, namedRange{ + Name: name, + Range: rng{ + Start: position{Line: i, Character: utf16Len(line[:start])}, + End: position{Line: i, Character: utf16Len(line[:end])}, + }, + }) + } + } + return out +} + +func utf16Len(s string) int { + return len([]rune(s)) +} + +func lastPackageSegment(path string) string { + parts := strings.Split(path, ".") + return parts[len(parts)-1] +} + +func importAlias(imp lang.ImportDecl) string { + if imp.Alias != "" { + return imp.Alias + } + path := strings.TrimPrefix(imp.Path, "go.") + parts := strings.Split(path, ".") + return parts[len(parts)-1] +} + +func renderImportDetail(imp lang.ImportDecl) string { + if imp.Alias != "" { + return "import " + imp.Alias + " " + imp.Path + } + return "import " + imp.Path +} + +func renderFunctionSignature(fn lang.FunctionDecl) string { + return renderFunctionSignatureFromParts(fn.Name, fn.Params, fn.ReturnType) +} + +func renderFunctionSignatureFromParts(name string, fnParams []lang.Param, returnType string) string { + var params []string + for _, param := range fnParams { + params = append(params, param.Name+": "+param.Type) + } + signature := "fun " + name + "(" + strings.Join(params, ", ") + ")" + if returnType != "" && returnType != "Unit" { + signature += ": " + returnType + } + return signature +} + +func renderInterfaceSignature(decl lang.InterfaceDecl) string { + return "interface " + decl.Name +} + +func renderClassSignature(decl lang.ClassDecl) string { + var b strings.Builder + b.WriteString("class ") + b.WriteString(decl.Name) + + var fields []string + for _, field := range decl.Fields { + keyword := "val" + if field.Mutable { + keyword = "var" + } + fields = append(fields, keyword+" "+field.Name+": "+field.Type) + } + if len(decl.Fields) > 0 { + b.WriteString("(") + b.WriteString(strings.Join(fields, ", ")) + b.WriteString(")") + } + if len(decl.Parents) > 0 { + b.WriteString(": ") + b.WriteString(strings.Join(decl.Parents, ", ")) + } + return b.String() +} + +func renderWorkerSignature(decl lang.WorkerDecl) string { + return "worker " + decl.Name +} + +func renderMethodSignature(className string, fn lang.FunctionDecl) string { + return className + "." + renderFunctionSignature(fn) +} + +func renderFieldSignature(className string, field lang.FieldDecl) string { + keyword := "val" + if field.Mutable { + keyword = "var" + } + return className + "." + keyword + " " + field.Name + ": " + field.Type +} + +func renderWorkerFieldSignature(workerName string, field lang.WorkerFieldDecl) string { + keyword := "val" + if field.Mutable { + keyword = "var" + } + typ := field.Type + if typ == "" { + typ = inferWorkerFieldType(field) + } + if typ != "" { + return workerName + "." + keyword + " " + field.Name + ": " + typ + } + return workerName + "." + keyword + " " + field.Name +} + +func renderVariableSignature(name string, mutable bool, typ string) string { + keyword := "val" + if mutable { + keyword = "var" + } + if typ != "" { + return keyword + " " + name + ": " + typ + } + return keyword + " " + name +} + +func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo { + functions := map[string]lang.FunctionDecl{} + for _, fn := range program.Functions { + functions[fn.Name] = fn + } + classes := map[string]lang.ClassDecl{} + for _, class := range program.Classes { + classes[class.Name] = class + } + workers := map[string]lang.WorkerDecl{} + for _, worker := range program.Workers { + workers[worker.Name] = worker + } + + 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} + } + if _, ok := workers[callee.Name]; ok { + return []string{callee.Name} + } + switch callee.Name { + case "println": + return []string{"Unit"} + case "after", "every": + return []string{"Channel"} + case "Channel": + if len(call.TypeArgs) == 1 { + return []string{"Channel<" + call.TypeArgs[0] + ">"} + } + return []string{"Channel"} + case "listOf", "mutableListOf": + if len(call.TypeArgs) == 1 { + return []string{"List<" + call.TypeArgs[0] + ">"} + } + if len(call.Args) > 0 { + elemType := inferExprType(call.Args[0], scope) + for i := 1; i < len(call.Args); i++ { + argType := inferExprType(call.Args[i], scope) + if argType == "" || elemType == "" || argType != elemType { + elemType = "" + break + } + } + if elemType != "" { + return []string{"List<" + elemType + ">"} + } + } + return []string{"List"} + case "mapOf", "mutableMapOf": + if len(call.TypeArgs) == 2 { + return []string{"Map<" + call.TypeArgs[0] + ", " + call.TypeArgs[1] + ">"} + } + if len(call.Args) > 0 && len(call.Args)%2 == 0 { + keyType := inferExprType(call.Args[0], scope) + valType := inferExprType(call.Args[1], scope) + for i := 2; i < len(call.Args); i += 2 { + nextKeyType := inferExprType(call.Args[i], scope) + nextValType := inferExprType(call.Args[i+1], scope) + if keyType == "" || nextKeyType == "" || keyType != nextKeyType { + keyType = "" + } + if valType == "" || nextValType == "" || valType != nextValType { + valType = "" + } + } + if keyType != "" && valType != "" { + return []string{"Map<" + keyType + ", " + valType + ">"} + } + } + return []string{"Map"} + default: + return nil + } + case lang.SelectorExpr: + receiverType := inferExprType(callee.Receiver, scope) + if callee.Name == "read" { + if elemType, ok := channelElementType(receiverType); ok { + return []string{elemType} + } + return nil + } + if callee.Name == "send" { + return []string{"Unit"} + } + switch receiverType { + case "*sql.DB", "sql.DB": + switch callee.Name { + case "Ping": + return []string{"error"} + } + case "*bun.DB", "bun.DB": + switch callee.Name { + case "NewSelect": + return []string{"*bun.SelectQuery"} + case "NewCreateTable": + return []string{"*bun.CreateTableQuery"} + case "NewInsert": + return []string{"*bun.InsertQuery"} + } + case "*bun.SelectQuery", "bun.SelectQuery": + switch callee.Name { + case "ColumnExpr", "Model": + return []string{"*bun.SelectQuery"} + case "Scan": + return []string{"error"} + case "Count": + return []string{"Int", "error"} + } + case "*bun.CreateTableQuery", "bun.CreateTableQuery": + switch callee.Name { + case "Model", "IfNotExists": + return []string{"*bun.CreateTableQuery"} + case "Exec": + return []string{"sql.Result", "error"} + } + case "*bun.InsertQuery", "bun.InsertQuery": + switch callee.Name { + case "Model": + return []string{"*bun.InsertQuery"} + case "Exec": + return []string{"sql.Result", "error"} + } + } + if path, ok := selectorPathLang(call.Callee); ok { + switch path { + case "context.Background": + return []string{"context.Context"} + case "sql.OpenDB": + return []string{"*sql.DB"} + case "bun.NewDB": + return []string{"*bun.DB"} + case "pgdriver.NewConnector": + return []string{"pgdriver.Connector"} + } + } + } + return nil + } + + inferExprType = func(expr lang.Expr, scope map[string]string) string { + switch e := expr.(type) { + case lang.IntExpr: + return "Int" + case lang.StringExpr: + return "String" + case lang.BoolExpr: + return "Boolean" + case lang.NullExpr: + return "null" + case lang.IdentExpr: + return scope[e.Name] + case lang.CallExpr: + types := inferCallReturnTypes(e, scope) + if len(types) > 0 { + return types[0] + } + return "" + case lang.SelectorExpr: + receiverType := inferExprType(e.Receiver, scope) + if receiverType == "" { + return "" + } + trimmed := strings.TrimPrefix(receiverType, "*") + if class, ok := classes[trimmed]; ok { + for _, field := range class.Fields { + if field.Name == e.Name { + return field.Type + } + } + } + if worker, ok := workers[trimmed]; ok { + for _, field := range worker.Fields { + if field.Name == e.Name { + if field.Type != "" { + return field.Type + } + return inferWorkerFieldType(field) + } + } + } + return "" + default: + return "" + } + } + + walkStmts = func(stmts []lang.Stmt, scope map[string]string) { + for _, stmt := range stmts { + switch s := stmt.(type) { + case lang.VarDecl: + typ := s.Type + if typ == "" { + typ = inferExprType(s.Value, scope) + } + out = append(out, variableDeclInfo{Name: s.Name, Mutable: s.Mutable, Type: typ}) + scope[s.Name] = typ + case lang.MultiVarDecl: + types := []string(nil) + if call, ok := s.Value.(lang.CallExpr); ok { + types = inferCallReturnTypes(call, scope) + } + for i, name := range s.Names { + typ := "" + if i < len(types) { + typ = types[i] + } + out = append(out, variableDeclInfo{Name: name, Mutable: s.Mutable, Type: typ}) + scope[name] = typ + } + case lang.AddAssignStmt: + if _, ok := scope[s.Name]; !ok { + scope[s.Name] = "" + } + case lang.IfStmt: + thenScope := copyTypeScope(scope) + elseScope := copyTypeScope(scope) + walkStmts(s.Then, thenScope) + walkStmts(s.Else, elseScope) + case lang.WhileStmt: + bodyScope := copyTypeScope(scope) + walkStmts(s.Body, bodyScope) + case lang.SelectStmt: + for _, c := range s.Cases { + caseScope := copyTypeScope(scope) + if elemType, ok := channelElementType(inferExprType(c.Source, scope)); ok { + caseScope["it"] = elemType + } else { + caseScope["it"] = "any" + } + walkStmts(c.Body, caseScope) + } + case lang.TryCatchStmt: + tryScope := copyTypeScope(scope) + walkStmts(s.TryBody, tryScope) + catchScope := copyTypeScope(scope) + catchScope[s.CatchName] = s.CatchType + walkStmts(s.CatchBody, catchScope) + } + } + } + + for _, fn := range program.Functions { + scope := map[string]string{} + for _, param := range fn.Params { + scope[param.Name] = param.Type + } + walkStmts(fn.Body, scope) + } + for _, class := range program.Classes { + fieldScope := map[string]string{} + for _, field := range class.Fields { + fieldScope[field.Name] = field.Type + } + for _, method := range class.Methods { + scope := copyTypeScope(fieldScope) + for _, param := range method.Params { + scope[param.Name] = param.Type + } + walkStmts(method.Body, scope) + } + } + for _, worker := range program.Workers { + fieldScope := map[string]string{} + for _, field := range worker.Fields { + typ := field.Type + if typ == "" { + typ = inferWorkerFieldType(field) + } + fieldScope[field.Name] = typ + } + for _, method := range worker.Methods { + scope := copyTypeScope(fieldScope) + for _, param := range method.Params { + scope[param.Name] = param.Type + } + walkStmts(method.Body, scope) + } + } + return out +} + +func copyTypeScope(scope map[string]string) map[string]string { + dup := make(map[string]string, len(scope)) + for k, v := range scope { + dup[k] = v + } + return dup +} + +func channelElementType(typ string) (string, bool) { + typ = strings.TrimSpace(typ) + if !strings.HasPrefix(typ, "Channel<") || !strings.HasSuffix(typ, ">") { + return "", false + } + inner := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(typ, "Channel<"), ">")) + if inner == "" { + return "", false + } + return inner, true +} + +func staticIntExprValue(expr lang.Expr) (int, bool) { + switch e := expr.(type) { + case lang.IntExpr: + v, err := strconv.Atoi(e.Value) + if err != nil { + return 0, false + } + return v, true + case lang.UnaryExpr: + if e.Op != "-" { + return 0, false + } + v, ok := staticIntExprValue(e.Value) + if !ok { + return 0, false + } + return -v, true + default: + return 0, false + } +} + +func inferWorkerFieldType(field lang.WorkerFieldDecl) string { + if field.Type != "" { + return field.Type + } + switch field.Value.(type) { + case lang.IntExpr: + return "Int" + case lang.StringExpr: + return "String" + case lang.BoolExpr: + return "Boolean" + default: + return "" + } +} + +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) { + if state.program == nil { + return stdlibTarget{}, false + } + imports := map[string]string{} + for _, imp := range state.program.Imports { + if strings.HasPrefix(imp.Path, `"`) { + continue + } + imports[importAlias(imp)] = importPathToGoPath(imp.Path) + } + if len(imports) == 0 { + return stdlibTarget{}, false + } + + query, _, ok := goplsQueryAtPosition(state.text, pos) + if ok { + if alias, symbol, found := splitSelectorQuery(query); found { + if pkgPath, ok := imports[alias]; ok { + return stdlibTarget{ + PackagePath: pkgPath, + SymbolName: symbol, + }, true + } + if inferredAlias, ok := receiverImportAlias(state.program, alias); ok { + if pkgPath, ok := imports[inferredAlias]; ok { + return stdlibTarget{ + PackagePath: pkgPath, + SymbolName: symbol, + }, true + } + } + } + if pkgPath, ok := imports[query]; ok { + return stdlibTarget{ + PackagePath: pkgPath, + }, true + } + } + + word, _ := wordAtPosition(state.text, pos) + if word == "" { + return stdlibTarget{}, false + } + if pkgPath, ok := imports[word]; ok { + return stdlibTarget{ + PackagePath: pkgPath, + }, true + } + if _, r := wordAtPosition(state.text, pos); r != (rng{}) { + if rootAlias, ok := inferSelectorRootAlias(state.text, r); ok { + if pkgPath, ok := imports[rootAlias]; ok { + return stdlibTarget{ + PackagePath: pkgPath, + SymbolName: word, + }, true + } + if inferredAlias, ok := receiverImportAlias(state.program, rootAlias); ok { + if pkgPath, ok := imports[inferredAlias]; ok { + return stdlibTarget{ + PackagePath: pkgPath, + SymbolName: word, + }, true + } + } + } + } + return stdlibTarget{}, false +} + +func inferSelectorRootAlias(text string, tokenRange rng) (string, bool) { + runes := []rune(text) + start := positionToOffset(text, tokenRange.Start) + if start <= 0 || start > len(runes) { + return "", false + } + if runes[start-1] != '.' { + return "", false + } + + i := start - 2 + parensDepth := 0 + for i >= 0 { + ch := runes[i] + switch ch { + case ')': + parensDepth++ + i-- + continue + case '(': + if parensDepth > 0 { + parensDepth-- + i-- + continue + } + } + if parensDepth > 0 { + i-- + continue + } + if unicode.IsSpace(ch) { + i-- + continue + } + if isWordRune(ch) { + end := i + 1 + for i >= 0 && isWordRune(runes[i]) { + i-- + } + startWord := i + 1 + ident := string(runes[startWord:end]) + + j := i + for j >= 0 && unicode.IsSpace(runes[j]) { + j-- + } + if j >= 0 && runes[j] == '.' { + i = j - 1 + continue + } + return ident, true + } + return "", false + } + return "", false +} + +func receiverImportAlias(program *lang.Program, receiver string) (string, bool) { + for _, class := range program.Classes { + for _, field := range class.Fields { + if field.Name != receiver { + continue + } + if alias, ok := firstTypeImportAlias(field.Type); ok { + return alias, true + } + } + } + return "", false +} + +func firstTypeImportAlias(typ string) (string, bool) { + typ = strings.TrimSpace(typ) + for strings.HasPrefix(typ, "*") { + typ = strings.TrimPrefix(typ, "*") + } + dot := strings.Index(typ, ".") + if dot <= 0 { + return "", false + } + return typ[:dot], true +} + +func splitSelectorQuery(query string) (string, string, bool) { + index := strings.Index(query, ".") + if index <= 0 || index+1 >= len(query) { + return "", "", false + } + return query[:index], query[index+1:], true +} + +func findStdlibSymbol(target stdlibTarget) (stdlibSymbol, bool) { + dir, ok := resolvePackageDir(target.PackagePath) + if !ok { + return stdlibSymbol{}, false + } + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool { + return !strings.HasSuffix(info.Name(), "_test.go") + }, parser.ParseComments) + if err != nil || len(pkgs) == 0 { + return stdlibSymbol{}, false + } + + pkg := firstASTPackage(pkgs) + if pkg == nil { + return stdlibSymbol{}, false + } + files := packageFiles(pkg) + if len(files) == 0 { + return stdlibSymbol{}, false + } + + if target.SymbolName == "" { + file := files[0] + start := fset.Position(file.Name.Pos()) + end := fset.Position(file.Name.End()) + decl := "package " + pkg.Name + doc := strings.TrimSpace(commentText(file.Doc)) + return stdlibSymbol{ + FileName: start.Filename, + Start: tokenPosition(start), + End: tokenPosition(end), + Decl: decl, + Doc: doc, + }, true + } + + for _, file := range files { + for _, decl := range file.Decls { + if sym, ok := matchStdlibDecl(fset, decl, target.SymbolName); ok { + return sym, true + } + } + } + return stdlibSymbol{}, false +} + +func resolvePackageDir(packagePath string) (string, bool) { + stdlibDir := filepath.Join(resolveGoRoot(), "src", filepath.FromSlash(packagePath)) + if info, err := os.Stat(stdlibDir); err == nil && info.IsDir() { + return stdlibDir, true + } + + if listed := goListPackageDir(packagePath); listed != "" { + return listed, true + } + + if cached := findInModuleCache(packagePath); cached != "" { + return cached, true + } + + return "", false +} + +func goListPackageDir(packagePath string) string { + output, err := exec.Command("go", "list", "-f", "{{.Dir}}", packagePath).Output() + if err != nil { + return "" + } + dir := strings.TrimSpace(string(output)) + if dir == "" { + return "" + } + if info, err := os.Stat(dir); err == nil && info.IsDir() { + return dir + } + return "" +} + +func findInModuleCache(packagePath string) string { + modCache := strings.TrimSpace(goEnv("GOMODCACHE")) + if modCache == "" { + goPath := strings.TrimSpace(goEnv("GOPATH")) + if goPath != "" { + parts := filepath.SplitList(goPath) + if len(parts) > 0 { + modCache = filepath.Join(parts[0], "pkg", "mod") + } + } + } + if modCache == "" { + return "" + } + + parts := strings.Split(packagePath, "/") + for i := len(parts); i >= 1; i-- { + modulePath := strings.Join(parts[:i], "/") + subPath := strings.Join(parts[i:], "/") + pattern := filepath.Join(modCache, escapeModulePath(modulePath)+"@*") + matches, _ := filepath.Glob(pattern) + if len(matches) == 0 { + continue + } + sort.Strings(matches) + for j := len(matches) - 1; j >= 0; j-- { + candidate := matches[j] + if subPath != "" { + candidate = filepath.Join(candidate, filepath.FromSlash(subPath)) + } + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate + } + } + } + return "" +} + +func escapeModulePath(path string) string { + var b strings.Builder + for _, r := range path { + if r >= 'A' && r <= 'Z' { + b.WriteRune('!') + b.WriteRune(r + ('a' - 'A')) + continue + } + b.WriteRune(r) + } + return b.String() +} + +func importPathToGoPath(path string) string { + trimmed := strings.TrimPrefix(path, "go.") + parts := strings.Split(trimmed, ".") + if len(parts) >= 3 && isDomainTLD(parts[1]) { + return parts[0] + "." + parts[1] + "/" + strings.Join(parts[2:], "/") + } + return strings.ReplaceAll(trimmed, ".", "/") +} + +func isDomainTLD(segment string) bool { + switch segment { + case "com", "org", "net", "io", "dev", "app", "ai": + return true + default: + return false + } +} + +func firstASTPackage(pkgs map[string]*ast.Package) *ast.Package { + names := make([]string, 0, len(pkgs)) + for name := range pkgs { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + return pkgs[name] + } + return nil +} + +func packageFiles(pkg *ast.Package) []*ast.File { + names := make([]string, 0, len(pkg.Files)) + for name := range pkg.Files { + names = append(names, name) + } + sort.Strings(names) + files := make([]*ast.File, 0, len(names)) + for _, name := range names { + files = append(files, pkg.Files[name]) + } + return files +} + +func matchStdlibDecl(fset *token.FileSet, decl ast.Decl, symbolName string) (stdlibSymbol, bool) { + switch d := decl.(type) { + case *ast.FuncDecl: + if d.Name == nil || d.Name.Name != symbolName { + return stdlibSymbol{}, false + } + return newStdlibSymbol(fset, d.Name.Pos(), d.Name.End(), formatFuncDecl(fset, d), commentText(d.Doc)), true + case *ast.GenDecl: + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + if s.Name.Name != symbolName { + continue + } + return newStdlibSymbol(fset, s.Name.Pos(), s.Name.End(), formatGenDecl(fset, d, s), specCommentText(d.Doc, s.Doc, s.Comment)), true + case *ast.ValueSpec: + for _, name := range s.Names { + if name.Name != symbolName { + continue + } + return newStdlibSymbol(fset, name.Pos(), name.End(), formatGenDecl(fset, d, s), specCommentText(d.Doc, s.Doc, s.Comment)), true + } + } + } + } + return stdlibSymbol{}, false +} + +func newStdlibSymbol(fset *token.FileSet, start token.Pos, end token.Pos, decl string, doc string) stdlibSymbol { + startPos := fset.Position(start) + endPos := fset.Position(end) + return stdlibSymbol{ + FileName: startPos.Filename, + Start: tokenPosition(startPos), + End: tokenPosition(endPos), + Decl: strings.TrimSpace(decl), + Doc: strings.TrimSpace(doc), + } +} + +func formatFuncDecl(fset *token.FileSet, decl *ast.FuncDecl) string { + copyDecl := *decl + copyDecl.Body = nil + return formatNode(fset, ©Decl) +} + +func formatGenDecl(fset *token.FileSet, decl *ast.GenDecl, spec ast.Spec) string { + copyDecl := &ast.GenDecl{ + Tok: decl.Tok, + Specs: []ast.Spec{ + spec, + }, + } + return formatNode(fset, copyDecl) +} + +func formatNode(fset *token.FileSet, node any) string { + var buf bytes.Buffer + if err := format.Node(&buf, fset, node); err != nil { + return "" + } + return buf.String() +} + +func commentText(group *ast.CommentGroup) string { + if group == nil { + return "" + } + return strings.TrimSpace(group.Text()) +} + +func specCommentText(groups ...*ast.CommentGroup) string { + for _, group := range groups { + if text := commentText(group); text != "" { + return text + } + } + return "" +} + +func tokenPosition(pos token.Position) position { + return position{ + Line: pos.Line - 1, + Character: pos.Column - 1, + } +} + +func resolveGoRoot() string { + if value := strings.TrimSpace(os.Getenv("GOROOT")); value != "" { + return value + } + if value := strings.TrimSpace(goEnv("GOROOT")); value != "" { + return value + } + return runtime.GOROOT() +} + +func copyScope(scope map[string]bool) map[string]bool { + dup := make(map[string]bool, len(scope)) + for k, v := range scope { + dup[k] = v + } + return dup +} + +func isBuiltin(name string) bool { + switch name { + case "println", "runCatching", "Channel", "after", "every", "listOf", "mutableListOf", "mapOf", "mutableMapOf": + return true + default: + return false + } +} + +func builtinHoverDetail(name string) (string, bool) { + switch name { + case "after": + return "fun after(ms: Int): Channel", true + case "every": + return "fun every(ms: Int): Channel", true + default: + return "", false + } +} + +func contains(values []string, needle string) bool { + for _, value := range values { + if value == needle { + return true + } + } + return false +} + +func rangeContains(r rng, p position) bool { + if p.Line < r.Start.Line || p.Line > r.End.Line { + return false + } + if p.Line == r.Start.Line && p.Character < r.Start.Character { + return false + } + if p.Line == r.End.Line && p.Character > r.End.Character { + return false + } + return true +} + +func uniqueDiagnostics(input []diagnostic) []diagnostic { + seen := map[string]bool{} + var out []diagnostic + for _, d := range input { + key := fmt.Sprintf("%d:%d:%d:%d:%s", d.Range.Start.Line, d.Range.Start.Character, d.Range.End.Line, d.Range.End.Character, d.Message) + if seen[key] { + continue + } + seen[key] = true + out = append(out, d) + } + return out +} + +func resolveGoplsPath() string { + if configured := strings.TrimSpace(os.Getenv("GOTLIN_GOPLS_PATH")); configured != "" { + return configured + } + if path, err := exec.LookPath("gopls"); err == nil { + return path + } + for _, dir := range goBinCandidates() { + path := filepath.Join(dir, "gopls") + if info, err := os.Stat(path); err == nil && !info.IsDir() { + return path + } + } + return "" +} + +func goBinCandidates() []string { + var dirs []string + seen := map[string]bool{} + add := func(dir string) { + dir = strings.TrimSpace(dir) + if dir == "" || seen[dir] { + return + } + seen[dir] = true + dirs = append(dirs, dir) + } + + add(os.Getenv("GOBIN")) + for _, part := range filepath.SplitList(os.Getenv("GOPATH")) { + add(filepath.Join(part, "bin")) + } + if home, err := os.UserHomeDir(); err == nil { + add(filepath.Join(home, "go", "bin")) + } + for _, value := range []string{goEnv("GOBIN"), goEnv("GOPATH")} { + for _, part := range filepath.SplitList(value) { + if filepath.Base(part) == "bin" { + add(part) + continue + } + add(filepath.Join(part, "bin")) + } + } + return dirs +} + +func goEnv(name string) string { + output, err := exec.Command("go", "env", name).Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(output)) +} + +func goplsQueryAtPosition(text string, pos position) (string, int, bool) { + word, r := wordAtPosition(text, pos) + if word == "" { + return "", 0, false + } + startOffset := positionToOffset(text, r.Start) + runes := []rune(text) + + if startOffset > 0 && runes[startOffset-1] == '.' { + leftEnd := startOffset - 1 + leftStart := leftEnd - 1 + for leftStart >= 0 && isWordRune(runes[leftStart]) { + leftStart-- + } + leftStart++ + if leftStart < leftEnd { + receiver := string(runes[leftStart:leftEnd]) + return receiver + "." + word, startOffset, true + } + } + + return word, startOffset, true +} + +func prepareGoplsSource(program *lang.Program, gotlinSource string, query string, tokenStart int) (string, int, func(), error) { + goSrc, err := lang.GenerateGo(program) + if err != nil { + return "", 0, func() {}, err + } + + preferredLine := offsetToPosition(gotlinSource, tokenStart).Line + targetOffset := findQueryOffset(string(goSrc), query, preferredLine) + if targetOffset < 0 { + targetOffset = findQueryOffset(string(goSrc), lastQuerySegment(query), preferredLine) + } + if targetOffset < 0 { + return "", 0, func() {}, fmt.Errorf("query %q not found in generated Go", query) + } + + dir, err := os.MkdirTemp("", "gotlin-gopls-*") + if err != nil { + return "", 0, func() {}, err + } + cleanup := func() { _ = os.RemoveAll(dir) } + + source := filepath.Join(dir, "main.go") + if err := os.WriteFile(source, goSrc, 0o644); err != nil { + cleanup() + return "", 0, func() {}, err + } + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module gotlin-gopls-temp\n\ngo 1.25\n"), 0o644); err != nil { + cleanup() + return "", 0, func() {}, err + } + + return source, targetOffset, cleanup, nil +} + +func findQueryOffset(source string, query string, preferredLine int) int { + if query == "" { + return -1 + } + indices := allQueryOffsets(source, query) + if len(indices) == 0 { + return -1 + } + bestIndex := indices[0] + bestDistance := absInt(byteLineNumber(source, bestIndex) - preferredLine) + for _, index := range indices[1:] { + distance := absInt(byteLineNumber(source, index) - preferredLine) + if distance < bestDistance { + bestIndex = index + bestDistance = distance + } + } + return bestIndex +} + +func lastQuerySegment(query string) string { + if idx := strings.LastIndex(query, "."); idx >= 0 && idx+1 < len(query) { + return query[idx+1:] + } + return query +} + +func parseGoplsDefinition(output string) (location, bool) { + line := strings.TrimSpace(output) + if line == "" { + return location{}, false + } + first := strings.Split(line, "\n")[0] + re := regexp.MustCompile(`^(.*):(\d+):(\d+)-(\d+):`) + match := re.FindStringSubmatch(first) + if len(match) != 5 { + return location{}, false + } + lineNo, err1 := strconv.Atoi(match[2]) + startCol, err2 := strconv.Atoi(match[3]) + endCol, err3 := strconv.Atoi(match[4]) + if err1 != nil || err2 != nil || err3 != nil { + return location{}, false + } + path := filepath.Clean(match[1]) + return location{ + URI: "file://" + path, + Range: rng{ + Start: position{Line: lineNo - 1, Character: startCol - 1}, + End: position{Line: lineNo - 1, Character: endCol - 1}, + }, + }, true +} + +func isTempGoplsLocation(loc location, tempDir string) bool { + path := strings.TrimPrefix(loc.URI, "file://") + cleanPath := filepath.Clean(path) + cleanTempDir := filepath.Clean(tempDir) + return cleanPath == cleanTempDir || strings.HasPrefix(cleanPath, cleanTempDir+string(os.PathSeparator)) +} + +func allQueryOffsets(source string, query string) []int { + var offsets []int + for start := 0; start < len(source); { + index := strings.Index(source[start:], query) + if index < 0 { + break + } + offset := start + index + offsets = append(offsets, offset) + start = offset + len(query) + } + return offsets +} + +func byteLineNumber(source string, offset int) int { + if offset < 0 { + offset = 0 + } + if offset > len(source) { + offset = len(source) + } + line := 0 + for i := 0; i < offset; i++ { + if source[i] == '\n' { + line++ + } + } + return line +} + +func absInt(n int) int { + if n < 0 { + return -n + } + return n +} diff --git a/cmd/gotlin-lsp/main_test.go b/cmd/gotlin-lsp/main_test.go new file mode 100644 index 0000000..b24e4a7 --- /dev/null +++ b/cmd/gotlin-lsp/main_test.go @@ -0,0 +1,841 @@ +package main + +import ( + "strings" + "testing" + + "gotlin/internal/lang" +) + +func TestResolveStdlibTarget(t *testing.T) { + text := strings.TrimSpace(` +package examples.http + +import fmt +import net.http + +fun main() { + fmt.Println("hello") + http.HandleFunc("/") { w, r -> helloHandler(w, r) } +} +`) + + program, err := lang.Parse(text) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + state := documentState{ + text: text, + program: program, + } + + printPos := position{Line: 6, Character: 9} + target, ok := resolveStdlibTarget(state, printPos) + if !ok { + t.Fatal("expected fmt.Println to resolve as stdlib target") + } + if target.PackagePath != "fmt" || target.SymbolName != "Println" { + t.Fatalf("unexpected target: %+v", target) + } + + httpPos := position{Line: 7, Character: 6} + target, ok = resolveStdlibTarget(state, httpPos) + if !ok { + t.Fatal("expected import alias to resolve as stdlib package") + } + if target.PackagePath != "net/http" || target.SymbolName != "" { + t.Fatalf("unexpected package target: %+v", target) + } +} + +func TestFindStdlibSymbol(t *testing.T) { + sym, ok := findStdlibSymbol(stdlibTarget{ + PackagePath: "fmt", + SymbolName: "Println", + }) + if !ok { + t.Fatal("expected fmt.Println to resolve in local Go stdlib") + } + if !strings.HasSuffix(sym.FileName, "/src/fmt/print.go") { + t.Fatalf("unexpected file: %s", sym.FileName) + } + if !strings.Contains(sym.Decl, "func Println") { + t.Fatalf("unexpected decl: %s", sym.Decl) + } +} + +func TestFindStdlibSymbolMethod(t *testing.T) { + sym, ok := findStdlibSymbol(stdlibTarget{ + PackagePath: "bytes", + SymbolName: "WriteString", + }) + if !ok { + t.Fatal("expected bytes.(*Buffer).WriteString to resolve in local Go sources") + } + if !strings.Contains(sym.Decl, "WriteString") { + t.Fatalf("unexpected decl: %s", sym.Decl) + } +} + +func TestResolveStdlibTargetThirdPartyImportPathMapping(t *testing.T) { + text := strings.TrimSpace(` +package demo + +import github.com.uptrace.bun + +fun main() { + bun.NewDB(nil, nil) +} +`) + + program, err := lang.Parse(text) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + state := documentState{ + text: text, + program: program, + } + + pos := position{Line: 5, Character: 8} + target, ok := resolveStdlibTarget(state, pos) + if !ok { + t.Fatal("expected bun.NewDB to resolve as package target") + } + if target.PackagePath != "github.com/uptrace/bun" || target.SymbolName != "NewDB" { + t.Fatalf("unexpected target: %+v", target) + } +} + +func TestResolveStdlibTargetFieldSelectorImportMapping(t *testing.T) { + text := strings.TrimSpace(` +package demo + +import github.com.uptrace.bun + +class EpicControllerImpl(val db: *bun.DB) { + fun bunHealth() { + db.NewSelect() + } +} +`) + + program, err := lang.Parse(text) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + state := documentState{ + text: text, + program: program, + } + + pos := position{Line: 6, Character: 12} + target, ok := resolveStdlibTarget(state, pos) + if !ok { + t.Fatal("expected db.NewSelect to resolve as imported package symbol") + } + if target.PackagePath != "github.com/uptrace/bun" || target.SymbolName != "NewSelect" { + t.Fatalf("unexpected target: %+v", target) + } +} + +func TestResolveStdlibTargetNestedFieldSelectorImportMapping(t *testing.T) { + text := strings.TrimSpace(` +package demo + +import context +import github.com.uptrace.bun + +class EpicControllerImpl(val db: *bun.DB) { + fun bunHealth() { + val ctx = context.Background() + db.NewSelect().ColumnExpr("1").Scan(ctx) + } +} +`) + + program, err := lang.Parse(text) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + state := documentState{ + text: text, + program: program, + } + + pos := position{Line: 8, Character: 40} + target, ok := resolveStdlibTarget(state, pos) + if !ok { + t.Fatal("expected nested db selector to resolve as imported package symbol") + } + if target.PackagePath != "github.com/uptrace/bun" || target.SymbolName != "Scan" { + t.Fatalf("unexpected target: %+v", target) + } +} + +func TestBuildDocumentStateIndexesClassesAndInterfaces(t *testing.T) { + text := strings.TrimSpace(` +package demo + +interface Greeter { + fun greet(name: String): String +} + +class ConsoleGreeter(val prefix: String) { + fun greet(name: String): String { + return prefix + name + } +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + + wantKinds := map[string]int{ + "Greeter": symbolKindInterface, + "ConsoleGreeter": symbolKindClass, + "greet": symbolKindMethod, + } + for name, kind := range wantKinds { + found := false + for _, sym := range state.symbols { + if sym.Name == name && sym.Kind == kind { + found = true + break + } + } + if !found { + t.Fatalf("missing symbol %q of kind %d", name, kind) + } + } +} + +func TestBuildDocumentStateClassSemantics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +interface Greeter { + fun greet(name: String): String +} + +class ConsoleGreeter(val prefix: String) { + fun greet(name: String): String { + return prefix + name + } +} + +fun main() { + val greeter: Greeter = ConsoleGreeter("hello, ") + println(greeter.greet("gotlin")) +} +`) + + state := buildDocumentState(text) + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } +} + +func TestBuildDocumentStateHTTPServerClassSyntax(t *testing.T) { + text := strings.TrimSpace(` +package examples.http + +import fmt +import net.http + +interface EpicController { + fun hello(w: http.ResponseWriter, r: *http.Request) +} + +class EpicControllerImpl: EpicController { + override fun hello(w: http.ResponseWriter, r: *http.Request) { + fmt.Fprintln(w, "hello from gotlin") + } +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } + + foundClass := false + foundMethod := false + for _, sym := range state.symbols { + if sym.Name == "EpicControllerImpl" && sym.Kind == symbolKindClass { + foundClass = true + if sym.Detail != "class EpicControllerImpl: EpicController" { + t.Fatalf("unexpected class detail: %q", sym.Detail) + } + } + if sym.Name == "hello" && sym.Kind == symbolKindMethod && strings.Contains(sym.Detail, "EpicControllerImpl.fun hello") { + foundMethod = true + } + } + + if !foundClass { + t.Fatal("expected EpicControllerImpl class symbol") + } + if !foundMethod { + t.Fatal("expected EpicControllerImpl.hello method symbol") + } +} + +func TestBuildDocumentStateIndexesClassFieldSymbols(t *testing.T) { + text := strings.TrimSpace(` +package demo + +import github.com.uptrace.bun + +class EpicControllerImpl(val db: *bun.DB) { + fun hello() { + println(db) + } +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + + foundField := false + for _, sym := range state.symbols { + if sym.Name == "db" && sym.Kind == symbolKindField { + foundField = true + if !strings.Contains(sym.Detail, "EpicControllerImpl.val db: *bun.DB") { + t.Fatalf("unexpected field detail: %q", sym.Detail) + } + break + } + } + if !foundField { + t.Fatal("expected db field symbol") + } +} + +func TestBuildDocumentStateIndexesLocalVariableSymbols(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun main() { + val ctx = "x" + var total = 1 + println(ctx) + println(total) +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + + foundCtx := false + foundTotal := false + for _, sym := range state.symbols { + if sym.Name == "ctx" && sym.Kind == symbolKindVariable { + foundCtx = true + if sym.Detail != "val ctx: String" { + t.Fatalf("unexpected ctx detail: %q", sym.Detail) + } + } + if sym.Name == "total" && sym.Kind == symbolKindVariable { + foundTotal = true + if sym.Detail != "var total: Int" { + t.Fatalf("unexpected total detail: %q", sym.Detail) + } + } + } + if !foundCtx || !foundTotal { + t.Fatalf("expected local variable symbols, got %+v", state.symbols) + } +} + +func TestBuildDocumentStateInfersHttpServerVariableTypes(t *testing.T) { + text := strings.TrimSpace(` +package demo + +import context +import github.com.uptrace.bun + +class C(val db: *bun.DB) { + fun run() { + val ctx = context.Background() + val total = db.NewSelect().ColumnExpr("1").Count(ctx) + println(total) + } +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + + seenCtx := false + seenTotal := false + for _, sym := range state.symbols { + if sym.Kind != symbolKindVariable { + continue + } + if sym.Name == "ctx" { + seenCtx = true + if sym.Detail != "val ctx: context.Context" { + t.Fatalf("unexpected ctx detail: %q", sym.Detail) + } + } + if sym.Name == "total" { + seenTotal = true + if sym.Detail != "val total: Int" { + t.Fatalf("unexpected total detail: %q", sym.Detail) + } + } + } + if !seenCtx || !seenTotal { + t.Fatalf("missing inferred variable symbols; got %+v", state.symbols) + } +} + +func TestBuildDocumentStateVariableTypeMatchingByName(t *testing.T) { + text := strings.TrimSpace(` +package demo + +class BunUser(val Name: String) +class EpicControllerImpl + +fun main() { + val epicController = EpicControllerImpl() + val user = BunUser("user-from-gotlin") + println(epicController) + println(user) +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + + var userDetail string + var controllerDetail string + for _, sym := range state.symbols { + if sym.Kind != symbolKindVariable { + continue + } + if sym.Name == "user" { + userDetail = sym.Detail + } + if sym.Name == "epicController" { + controllerDetail = sym.Detail + } + } + + if userDetail != "val user: BunUser" { + t.Fatalf("unexpected user detail: %q", userDetail) + } + if controllerDetail != "val epicController: EpicControllerImpl" { + t.Fatalf("unexpected epicController detail: %q", controllerDetail) + } +} + +func TestBuildDocumentStateMultiAssignAndNullSemantics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +import database.sql + +fun main() { + val db, err = sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable") + if (err != null) { + return + } + db.Close() +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } +} + +func TestBuildDocumentStateExceptionsAndRunCatchingSemantics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun mayFail() { + throw "boom" +} + +fun main() { + val result = runCatching({ mayFail() }) + if (result.isSuccess()) { + println("ok") + } else { + println(result.exceptionOrNull()) + } + try { + mayFail() + } catch (e: String) { + println(e) + } +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } +} + +func TestBuildDocumentStateCollectionLiteralBuiltins(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun main() { + val names = listOf("alice", "bob") + val test = listOf(1, 7) + val ages = mapOf("alice", 30, "bob", 25) + println(names) + println(test) + println(ages) +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } + + var namesDetail string + var testDetail string + var agesDetail string + for _, sym := range state.symbols { + if sym.Kind != symbolKindVariable { + continue + } + if sym.Name == "names" { + namesDetail = sym.Detail + } + if sym.Name == "test" { + testDetail = sym.Detail + } + if sym.Name == "ages" { + agesDetail = sym.Detail + } + } + if namesDetail != "val names: List" { + t.Fatalf("unexpected names detail: %q", namesDetail) + } + if testDetail != "val test: List" { + t.Fatalf("unexpected test detail: %q", testDetail) + } + if agesDetail != "val ages: Map" { + t.Fatalf("unexpected ages detail: %q", agesDetail) + } +} + +func TestBuildDocumentStateCollectionLiteralBuiltinsWithTypeArgs(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun main() { + val test = listOf() + val labels = mapOf() + println(test) + println(labels) +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } + + var testDetail string + var labelsDetail string + for _, sym := range state.symbols { + if sym.Kind != symbolKindVariable { + continue + } + if sym.Name == "test" { + testDetail = sym.Detail + } + if sym.Name == "labels" { + labelsDetail = sym.Detail + } + } + + if testDetail != "val test: List" { + t.Fatalf("unexpected test detail: %q", testDetail) + } + if labelsDetail != "val labels: Map" { + t.Fatalf("unexpected labels detail: %q", labelsDetail) + } +} + +func TestBuildDocumentStateGoStatementSemantics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun runWorker(name: String) { + println(name) +} + +fun main() { + go runWorker("alice") +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } +} + +func TestBuildDocumentStateChannelsAndSelectSemantics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun writer(ch: Channel) { + ch.send(7) +} + +fun main() { + val ch = Channel() + go writer(ch) + select { + ch -> println(it) + } + val v = ch.read() + println(v) +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } + + var chDetail string + var vDetail string + for _, sym := range state.symbols { + if sym.Kind != symbolKindVariable { + continue + } + if sym.Name == "ch" { + chDetail = sym.Detail + } + if sym.Name == "v" { + vDetail = sym.Detail + } + } + if chDetail != "val ch: Channel" { + t.Fatalf("unexpected ch detail: %q", chDetail) + } + if vDetail != "val v: Int" { + t.Fatalf("unexpected v detail: %q", vDetail) + } +} + +func TestBuildDocumentStateWorkerSemantics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +worker Counter { + val counter = 0 + + fun getCount(): Int { + return counter + } + fun increment() { + counter += 1 + } +} + +fun main() { + val counter = Counter() + counter.increment() + println(counter.getCount()) +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } + + var counterDetail string + for _, sym := range state.symbols { + if sym.Kind == symbolKindVariable && sym.Name == "counter" { + counterDetail = sym.Detail + break + } + } + if counterDetail != "val counter: Counter" { + t.Fatalf("unexpected counter detail: %q", counterDetail) + } +} + +func TestBuildDocumentStateTimerBuiltinsSemantics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun main() { + val once = after(1000) + val repeat = every(250) + select { + after(1000) -> println(it) + every(250) -> println("tick") + } + println(once) + println(repeat) +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) != 0 { + t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) + } + + var onceDetail string + var repeatDetail string + for _, sym := range state.symbols { + if sym.Kind != symbolKindVariable { + continue + } + if sym.Name == "once" { + onceDetail = sym.Detail + } + if sym.Name == "repeat" { + repeatDetail = sym.Detail + } + } + if onceDetail != "val once: Channel" { + t.Fatalf("unexpected once detail: %q", onceDetail) + } + if repeatDetail != "val repeat: Channel" { + t.Fatalf("unexpected repeat detail: %q", repeatDetail) + } +} + +func TestBuildDocumentStateEveryNonPositiveDiagnostics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun main() { + select { + every(0) -> println("x") + } +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + if len(state.diagnostics) == 0 { + t.Fatal("expected diagnostics") + } + found := false + for _, d := range state.diagnostics { + if strings.Contains(d.Message, "every(ms) requires ms > 0") { + found = true + break + } + } + if !found { + t.Fatalf("expected every(ms) diagnostic, got %+v", state.diagnostics) + } +} + +func TestBuildDocumentStateAfterIntArgumentDiagnostics(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun main() { + select { + after("1s") -> println("x") + } +} +`) + + state := buildDocumentState(text) + if state.program == nil { + t.Fatal("expected parsed program") + } + found := false + for _, d := range state.diagnostics { + if strings.Contains(d.Message, "after(ms) expects an Int argument") { + found = true + break + } + } + if !found { + t.Fatalf("expected after int argument diagnostic, got %+v", state.diagnostics) + } +} + +func TestHoverBuiltinEverySignature(t *testing.T) { + text := strings.TrimSpace(` +package demo + +fun main() { + every(100) +} +`) + uri := "file:///tmp/demo.gt" + s := &server{ + docs: map[string]documentState{ + uri: buildDocumentState(text), + }, + } + + result := s.hover(uri, position{Line: 3, Character: 5}) + if result == nil { + t.Fatal("expected hover result") + } + m, ok := result.(map[string]any) + if !ok { + t.Fatalf("unexpected hover type: %T", result) + } + contents, ok := m["contents"].(map[string]any) + if !ok { + t.Fatalf("unexpected hover contents: %+v", m) + } + value, _ := contents["value"].(string) + if !strings.Contains(value, "fun every(ms: Int): Channel") { + t.Fatalf("unexpected hover value: %q", value) + } +} diff --git a/cmd/gotlinc/main.go b/cmd/gotlinc/main.go new file mode 100644 index 0000000..cd2c7f7 --- /dev/null +++ b/cmd/gotlinc/main.go @@ -0,0 +1,247 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + + "gotlin/internal/lang" +) + +func main() { + if len(os.Args) < 2 { + usage() + } + + switch os.Args[1] { + case "build": + runBuild(os.Args[2:]) + case "run": + runRun(os.Args[2:]) + default: + runBuild(os.Args[1:]) + } +} + +func runBuild(args []string) { + fs := flag.NewFlagSet("build", flag.ExitOnError) + srcOnly := fs.Bool("src", false, "emit Go source instead of building an executable") + outPath := fs.String("o", "", "output file path") + fs.Usage = func() { + fmt.Fprintln(os.Stderr, "usage: gotlinc build [-src] [-o output] ") + } + normalizedArgs, err := normalizeBuildArgs(args) + if err != nil { + fail(err) + } + if err := fs.Parse(normalizedArgs); err != nil { + fail(err) + } + if fs.NArg() != 1 { + fs.Usage() + os.Exit(2) + } + + inputPath := fs.Arg(0) + goSrc := compileFile(inputPath, !*srcOnly) + + if *srcOnly { + if *outPath == "" { + _, _ = os.Stdout.WriteString(goSrc) + return + } + if err := os.WriteFile(*outPath, []byte(goSrc), 0o644); err != nil { + fail(err) + } + return + } + + outputPath := *outPath + if outputPath == "" { + outputPath = defaultExecutablePath(inputPath) + } + + if err := buildExecutable(goSrc, outputPath); err != nil { + fail(err) + } +} + +func runRun(args []string) { + fs := flag.NewFlagSet("run", flag.ExitOnError) + fs.Usage = func() { + fmt.Fprintln(os.Stderr, "usage: gotlinc run [program args...]") + } + if err := fs.Parse(args); err != nil { + fail(err) + } + if fs.NArg() < 1 { + fs.Usage() + os.Exit(2) + } + + inputPath := fs.Arg(0) + goSrc := compileFile(inputPath, true) + + tmpFile, err := os.CreateTemp("", "gotlinc-*.go") + if err != nil { + fail(err) + } + tmpPath := tmpFile.Name() + defer os.Remove(tmpPath) + + if _, err := tmpFile.WriteString(goSrc); err != nil { + _ = tmpFile.Close() + fail(err) + } + if err := tmpFile.Close(); err != nil { + fail(err) + } + + cmdArgs := append([]string{"run", tmpPath}, fs.Args()[1:]...) + cmd := exec.Command("go", cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if err := cmd.Run(); err != nil { + fail(err) + } +} + +func compileFile(inputPath string, forceMain bool) string { + src, err := os.ReadFile(inputPath) + if err != nil { + fail(err) + } + + program, err := lang.Parse(string(src)) + if err != nil { + fail(err) + } + + var goSrc []byte + if forceMain { + goSrc, err = lang.GenerateGoMain(program) + } else { + goSrc, err = lang.GenerateGo(program) + } + if err != nil { + fail(err) + } + if forceMain { + return addBestEffortLineDirectives(string(goSrc), inputPath, string(src)) + } + return string(goSrc) +} + +func buildExecutable(goSrc string, outputPath string) error { + tmpFile, err := os.CreateTemp("", "gotlinc-build-*.go") + if err != nil { + return err + } + tmpPath := tmpFile.Name() + defer os.Remove(tmpPath) + + if _, err := tmpFile.WriteString(goSrc); err != nil { + _ = tmpFile.Close() + return err + } + if err := tmpFile.Close(); err != nil { + return err + } + + cmd := exec.Command("go", "build", "-o", outputPath, tmpPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd.Run() +} + +func defaultExecutablePath(inputPath string) string { + base := strings.TrimSuffix(filepath.Base(inputPath), filepath.Ext(inputPath)) + if runtime.GOOS == "windows" && filepath.Ext(base) != ".exe" { + base += ".exe" + } + return base +} + +func normalizeBuildArgs(args []string) ([]string, error) { + var flags []string + var positional []string + + for i := 0; i < len(args); i++ { + arg := args[i] + switch { + case arg == "-src": + flags = append(flags, arg) + case arg == "-o": + if i+1 >= len(args) { + return nil, fmt.Errorf("missing value for -o") + } + flags = append(flags, arg, args[i+1]) + i++ + case strings.HasPrefix(arg, "-o="): + flags = append(flags, arg) + case strings.HasPrefix(arg, "-"): + flags = append(flags, arg) + default: + positional = append(positional, arg) + } + } + + return append(flags, positional...), nil +} + +func usage() { + fmt.Fprintln(os.Stderr, "usage:") + fmt.Fprintln(os.Stderr, " gotlinc build [-src] [-o output] ") + fmt.Fprintln(os.Stderr, " gotlinc run [program args...]") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "default command: build") + os.Exit(2) +} + +func fail(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func addBestEffortLineDirectives(goSrc string, inputPath string, gtSrc string) string { + fnLines := sourceFunctionLines(gtSrc) + if len(fnLines) == 0 { + return goSrc + } + + pattern := regexp.MustCompile(`^\s*func\s+(?:\(\s*[^)]*\)\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(`) + lines := strings.Split(goSrc, "\n") + out := make([]string, 0, len(lines)+len(fnLines)) + for _, line := range lines { + m := pattern.FindStringSubmatch(line) + if len(m) == 2 { + if gtLine, ok := fnLines[m[1]]; ok { + out = append(out, fmt.Sprintf("//line %s:%d", inputPath, gtLine)) + } + } + out = append(out, line) + } + return strings.Join(out, "\n") +} + +func sourceFunctionLines(src string) map[string]int { + out := map[string]int{} + pattern := regexp.MustCompile(`^\s*fun\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`) + lines := strings.Split(src, "\n") + for i, line := range lines { + m := pattern.FindStringSubmatch(line) + if len(m) == 2 { + if _, exists := out[m[1]]; !exists { + out[m[1]] = i + 1 + } + } + } + return out +} diff --git a/examples/classes.gt b/examples/classes.gt new file mode 100644 index 0000000..93df271 --- /dev/null +++ b/examples/classes.gt @@ -0,0 +1,16 @@ +package demo + +interface Greeter { + fun greet(name: String): String +} + +class ConsoleGreeter(val prefix: String) { + fun greet(name: String): String { + return prefix + name + } +} + +fun main() { + val greeter: Greeter = ConsoleGreeter("hello, ") + println(greeter.greet("gotlin")) +} diff --git a/examples/hello.gt b/examples/hello.gt new file mode 100644 index 0000000..1cf4feb --- /dev/null +++ b/examples/hello.gt @@ -0,0 +1,10 @@ +package examples.hello + +fun greet(name: String): String { + return "Hello, " + name +} + +fun main() { + val message = greet("Go backend") + println(message) +} diff --git a/examples/http_server.gt b/examples/http_server.gt new file mode 100644 index 0000000..e78c9b0 --- /dev/null +++ b/examples/http_server.gt @@ -0,0 +1,76 @@ +package examples.http + +import context +import fmt +import github.com.uptrace.bun +import github.com.uptrace.bun.dialect.pgdialect +import github.com.uptrace.bun.driver.pgdriver +import database.sql +import net.http +import os + +class BunUser(val Name: String) + +class EpicControllerImpl(val db: *bun.DB) { + + fun hello(w: http.ResponseWriter, r: *http.Request) { + fmt.Fprintln(w, "hello from gotlin") + } + + fun bunHealth(w: http.ResponseWriter, r: *http.Request) { + val ctx = context.Background() + db.NewSelect().ColumnExpr("1").Scan(ctx) + fmt.Fprintln(w, "bun ok") + } + + fun bunUsers(w: http.ResponseWriter, r: *http.Request) { + val ctx = context.Background() + val model = BunUser("gotlin") + + db.NewCreateTable().Model(model).IfNotExists().Exec(ctx) + val user = BunUser("user-from-gotlin") + db.NewInsert().Model(user).Exec(ctx) + val total = db.NewSelect().Model(model).Count(ctx) + + fmt.Fprintln(w, "bun users total:", total) + } +} + +worker Counter { + val counter = 0 + + fun getCount(): Int { + return counter + } + fun increment() { + counter += 1 + println(counter) + } +} + +fun main() { + val postgresDsn = "postgresql://postgres:postgres@localhost/postgres?sslmode=disable" + val sqlDb = sql.OpenDB( + pgdriver.NewConnector( + pgdriver.WithDSN(postgresDsn) + ) + ) + val db = bun.NewDB(sqlDb, pgdialect.New()) + val counter = Counter() + go { + while(true) { + select { + every(1000) -> counter.increment() + } + } + } + val epicController = EpicControllerImpl(db) + fmt.Println("serving http://localhost:8080") + http.HandleFunc("/", epicController.hello) + http.HandleFunc("/bun", epicController.bunHealth) + http.HandleFunc("/bun/users", epicController.bunUsers) + http.HandleFunc("/counter") { w, r -> + fmt.Fprintln(w, "bun users total:", counter.getCount()) + } + http.ListenAndServe(":8080", http.DefaultServeMux) +} diff --git a/examples/imports.gt b/examples/imports.gt new file mode 100644 index 0000000..bc31d31 --- /dev/null +++ b/examples/imports.gt @@ -0,0 +1,8 @@ +package examples.imports + +import go.strings +import go.fmt + +fun main() { + fmt.Println(strings.ToUpper("gotlinn")) +} diff --git a/examples/lambdas.gt b/examples/lambdas.gt new file mode 100644 index 0000000..244aae7 --- /dev/null +++ b/examples/lambdas.gt @@ -0,0 +1,9 @@ +package examples.lambdas + +fun apply(value: String, fn: (String) -> Unit) { + fn(value) +} + +fun main() { + apply("from lambda", { println(it) }) +} diff --git a/examples/showcase.gt b/examples/showcase.gt new file mode 100644 index 0000000..0437a6a --- /dev/null +++ b/examples/showcase.gt @@ -0,0 +1,78 @@ +package examples.showcase + +import fmt +import strings + +interface Greeter { + fun greet(name: String): String +} + +class PrefixGreeter(val prefix: String): Greeter { + fun greet(name: String): String { + return prefix + " " + name + } +} + +worker Counter { + val count = 0 + + fun increment() { + count += 1 + } + + fun value(): Int { + return count + } +} + +fun risky(input: String): String { + if (input == "boom") { + throw "boom requested" + } + return input +} + +fun main() { + val greeter: Greeter = PrefixGreeter("hello") + println(greeter.greet("gotlin")) + + val names = listOf("ada", "linus") + val scores = mapOf("ada", 10, "linus", 8) + println(names) + println(scores) + + val upper = strings.ToUpper("gotlin") + fmt.Println("interop:", upper) + + val result = runCatching({ risky("boom") }) + if (result.isSuccess()) { + println("runCatching: success") + } else { + println("runCatching:") + println(result.exceptionOrNull()) + } + + val maybe: any = null + if (maybe == null) { + println("null check works") + } + + val counter = Counter() + counter.increment() + counter.increment() + fmt.Println("worker value:", counter.value()) + + val ready = Channel() + go { + select { + after(120) -> ready.send("timer fired") + } + } + select { + ready -> println("channel says: " + it) + } + + select { + every(50) -> println("one periodic tick") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7b22240 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module gotlin + +go 1.25.6 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/lib/pq v1.11.2 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/uptrace/bun v1.2.18 // indirect + github.com/uptrace/bun/dialect/pgdialect v1.2.18 // indirect + github.com/uptrace/bun/driver/pgdriver v1.2.18 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + mellium.im/sasl v0.3.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e9784ea --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.2.18 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU= +github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y= +github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k= +github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU= +github.com/uptrace/bun/driver/pgdriver v1.2.18 h1:Zojuc83ulApocXomBLEcx1DqCZweREafHCjPfyXo88I= +github.com/uptrace/bun/driver/pgdriver v1.2.18/go.mod h1:ZRJcARw93nxbQ5WawTrc5EO+F+GygkcYgDLEnT17CcE= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= +mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY= diff --git a/hello.go b/hello.go new file mode 100644 index 0000000..a74e6ba --- /dev/null +++ b/hello.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func greet(name string) string { + return "Hello, " + name +} + +func main() { + message := greet("Go backend") + fmt.Println(message) +} diff --git a/imports.go b/imports.go new file mode 100644 index 0000000..22f4097 --- /dev/null +++ b/imports.go @@ -0,0 +1,10 @@ +package main + +import ( + "fmt" + "strings" +) + +func main() { + fmt.Println(strings.ToUpper("gotlin")) +} diff --git a/internal/lang/ast.go b/internal/lang/ast.go new file mode 100644 index 0000000..af0046d --- /dev/null +++ b/internal/lang/ast.go @@ -0,0 +1,235 @@ +package lang + +type Program struct { + PackagePath string + Imports []ImportDecl + Interfaces []InterfaceDecl + Classes []ClassDecl + Workers []WorkerDecl + Functions []FunctionDecl +} + +type ImportDecl struct { + Alias string + Path string +} + +type InterfaceDecl struct { + Name string + Methods []FunctionSignature +} + +type ClassDecl struct { + Name string + Fields []FieldDecl + Parents []string + Methods []FunctionDecl +} + +type WorkerDecl struct { + Name string + Fields []WorkerFieldDecl + Methods []FunctionDecl +} + +type WorkerFieldDecl struct { + Mutable bool + Name string + Type string + Value Expr +} + +type FieldDecl struct { + Mutable bool + Name string + Type string +} + +type FunctionSignature struct { + Name string + Params []Param + ReturnType string +} + +type FunctionDecl struct { + Name string + Params []Param + ReturnType string + Body []Stmt +} + +type Param struct { + Name string + Type string +} + +type Stmt interface { + stmtNode() +} + +type Expr interface { + exprNode() +} + +type VarDecl struct { + Mutable bool + Name string + Type string + Value Expr +} + +func (VarDecl) stmtNode() {} + +type MultiVarDecl struct { + Mutable bool + Names []string + Value Expr +} + +func (MultiVarDecl) stmtNode() {} + +type AssignStmt struct { + Name string + Value Expr +} + +func (AssignStmt) stmtNode() {} + +type AddAssignStmt struct { + Name string + Value Expr +} + +func (AddAssignStmt) stmtNode() {} + +type MultiAssignStmt struct { + Names []string + Value Expr +} + +func (MultiAssignStmt) stmtNode() {} + +type ReturnStmt struct { + Value Expr +} + +func (ReturnStmt) stmtNode() {} + +type ThrowStmt struct { + Value Expr +} + +func (ThrowStmt) stmtNode() {} + +type GoStmt struct { + Value Expr +} + +func (GoStmt) stmtNode() {} + +type ExprStmt struct { + Value Expr +} + +func (ExprStmt) stmtNode() {} + +type IfStmt struct { + Cond Expr + Then []Stmt + Else []Stmt +} + +func (IfStmt) stmtNode() {} + +type WhileStmt struct { + Cond Expr + Body []Stmt +} + +func (WhileStmt) stmtNode() {} + +type TryCatchStmt struct { + TryBody []Stmt + CatchName string + CatchType string + CatchBody []Stmt +} + +func (TryCatchStmt) stmtNode() {} + +type SelectStmt struct { + Cases []SelectCase +} + +func (SelectStmt) stmtNode() {} + +type SelectCase struct { + Source Expr + Body []Stmt +} + +type IdentExpr struct { + Name string +} + +func (IdentExpr) exprNode() {} + +type IntExpr struct { + Value string +} + +func (IntExpr) exprNode() {} + +type StringExpr struct { + Value string +} + +func (StringExpr) exprNode() {} + +type BoolExpr struct { + Value bool +} + +func (BoolExpr) exprNode() {} + +type NullExpr struct{} + +func (NullExpr) exprNode() {} + +type UnaryExpr struct { + Op string + Value Expr +} + +func (UnaryExpr) exprNode() {} + +type BinaryExpr struct { + Left Expr + Op string + Right Expr +} + +func (BinaryExpr) exprNode() {} + +type CallExpr struct { + Callee Expr + Args []Expr + TypeArgs []string +} + +func (CallExpr) exprNode() {} + +type SelectorExpr struct { + Receiver Expr + Name string +} + +func (SelectorExpr) exprNode() {} + +type LambdaExpr struct { + Params []Param + ImplicitIt bool + Body []Stmt +} + +func (LambdaExpr) exprNode() {} diff --git a/internal/lang/compiler_test.go b/internal/lang/compiler_test.go new file mode 100644 index 0000000..89c0179 --- /dev/null +++ b/internal/lang/compiler_test.go @@ -0,0 +1,1189 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestGenerateGoHello(t *testing.T) { + src := ` +package demo + +fun greet(name: String): String { + return "hello, " + name +} + +fun main() { + val message = greet("world") + println(message) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `package demo`, + `func greet(name string) string`, + `return "hello, " + name`, + `fmt.Println(message)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoImports(t *testing.T) { + src := ` +package demo + +import strings +import rand math.rand + +fun main() { + val upper = strings.ToUpper("go") + println(upper) + println(rand.Intn(3)) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `"strings"`, + `rand "math/rand"`, + `upper := gotlinAutoThrow(strings.ToUpper("go"))`, + `fmt.Println(rand.Intn(3))`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoQualifiedTypes(t *testing.T) { + src := ` +package demo.web + +import fmt +import net.http + +fun helloHandler(w: http.ResponseWriter, r: *http.Request) { + fmt.Fprintln(w, "hello") +} + +fun main() { + http.HandleFunc("/", helloHandler) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `package web`, + `func helloHandler(w http.ResponseWriter, r *http.Request)`, + `fmt.Fprintln(w, "hello")`, + `http.HandleFunc("/", helloHandler)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoTypedLambda(t *testing.T) { + src := ` +package demo + +import fmt +import net.http + +fun main() { + http.HandleFunc("/", { w: http.ResponseWriter, r: *http.Request -> + fmt.Fprintln(w, "hello from lambda") + }) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {`, + `fmt.Fprintln(w, "hello from lambda")`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoTrailingLambda(t *testing.T) { + src := ` +package demo + +fun apply(value: String, fn: (String) -> Unit) { + fn(value) +} + +fun main() { + apply("world") { value -> println(value) } +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `apply("world", func(value string) {`, + `fmt.Println(value)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoHandleFuncKotlinSyntax(t *testing.T) { + src := ` +package demo + +import fmt +import net.http + +fun helloHandler(w: http.ResponseWriter, r: *http.Request) { + fmt.Fprintln(w, "hello") +} + +fun main() { + http.HandleFunc("/") { w, r -> helloHandler(w, r) } +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {`, + `helloHandler(w, r)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoImplicitItLambda(t *testing.T) { + src := ` +package demo + +fun apply(value: String, fn: (String) -> Unit) { + fn(value) +} + +fun main() { + apply("world", { println(it) }) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `func apply(value string, fn func(string))`, + `apply("world", func(it string) {`, + `fmt.Println(it)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoClass(t *testing.T) { + src := ` +package demo + +class Greeter(val name: String) { + fun greet() { + println("hello, " + name) + println(this.name) + } +} + +fun main() { + val greeter = Greeter("world") + greeter.greet() +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `type Greeter struct {`, + `name string`, + `func NewGreeter(name string) *Greeter`, + `func (self *Greeter) greet() {`, + `fmt.Println("hello, " + self.name)`, + `fmt.Println(self.name)`, + `greeter := gotlinAutoThrow(NewGreeter("world"))`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoInterface(t *testing.T) { + src := ` +package demo + +interface Greeter { + fun greet(name: String): String +} + +class ConsoleGreeter(val prefix: String) { + fun greet(name: String): String { + return prefix + name + } +} + +fun greetAll(greeter: Greeter) { + println(greeter.greet("world")) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `type Greeter interface {`, + `greet(name string) string`, + `type ConsoleGreeter struct {`, + `func (self *ConsoleGreeter) greet(name string) string {`, + `return self.prefix + name`, + `func greetAll(greeter Greeter)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoHTTPServerWithInterfaceMethodHandler(t *testing.T) { + src := ` +package examples.http + +import fmt +import net.http + +interface EpicController { + fun hello(w: http.ResponseWriter, r: *http.Request) +} + +class EpicControllerImpl: EpicController { + override fun hello(w: http.ResponseWriter, r: *http.Request) { + fmt.Fprintln(w, "hello from gotlin") + } +} + +fun main() { + val epicController: EpicController = EpicControllerImpl() + fmt.Println("serving http://localhost:8080") + http.HandleFunc("/", epicController.hello) + http.ListenAndServe(":8080", http.DefaultServeMux) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `type EpicController interface {`, + `hello(w http.ResponseWriter, r *http.Request)`, + `type EpicControllerImpl struct {`, + `func NewEpicControllerImpl() *EpicControllerImpl`, + `func (self *EpicControllerImpl) hello(w http.ResponseWriter, r *http.Request) {`, + `var epicController EpicController = gotlinAutoThrow(NewEpicControllerImpl())`, + `http.HandleFunc("/", epicController.hello)`, + `http.ListenAndServe(":8080", http.DefaultServeMux)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoAliasedStringImport(t *testing.T) { + src := ` +package demo + +import _ github.com.lib.pq +import database.sql + +fun main() { + println(sql.Drivers()) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `_ "github.com/lib/pq"`, + `"database/sql"`, + `fmt.Println(sql.Drivers())`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoMultiAssignAndNull(t *testing.T) { + src := ` +package demo + +import database.sql + +fun main() { + val db, err = sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable") + if (err != null) { + return + } + db.Close() +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `db, err := sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable")`, + `if err != nil {`, + `db.Close()`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoAutoBlankImportForSideEffects(t *testing.T) { + src := ` +package demo + +import github.com.lib.pq +import database.sql + +fun main() { + val db, err = sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable") + if (err != null) { + return + } + db.Close() +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `_ "github.com/lib/pq"`, + `"database/sql"`, + `db, err := sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable")`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoExceptionsAndRunCatching(t *testing.T) { + src := ` +package demo + +fun mayFail() { + throw "boom" +} + +fun main() { + val result = runCatching({ mayFail() }) + if (result.isSuccess()) { + println("ok") + } else { + println(result.exceptionOrNull()) + } + + try { + mayFail() + } catch (e: String) { + println(e) + } +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `type gotlinResult struct {`, + `func gotlinRunCatching(fn func()) (result gotlinResult) {`, + `panic("boom")`, + `result := gotlinAutoThrow(gotlinRunCatching(func() {`, + `if recovered := recover(); recovered != nil {`, + `e := recovered`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoAutoThrowForGoErrorResults(t *testing.T) { + src := ` +package demo + +import database.sql + +fun main() { + val db = sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable") + val err = db.Ping() + if (err != null) { + throw err + } +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `func gotlinAutoThrow[T any](value T, rest ...any) T {`, + `db := gotlinAutoThrow(sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable"))`, + `err := gotlinAutoThrow(db.Ping())`, + `if err != nil {`, + `panic(err)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoAutoThrowForErrorExprStmt(t *testing.T) { + src := ` +package demo + +import database.sql + +fun main() { + val db = sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable") + db.Ping() +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `db := gotlinAutoThrow(sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable"))`, + `db.Ping()`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoClassWithoutBodyBraces(t *testing.T) { + src := ` +package demo + +class User(val Name: String) + +fun main() { + val user = User("alice") + println(user.Name) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `type User struct {`, + `func NewUser(Name string) *User`, + `user := gotlinAutoThrow(NewUser("alice"))`, + `fmt.Println(user.Name)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoGenericListAndMapTypes(t *testing.T) { + src := ` +package demo + +class Store(val names: List, val counts: Map) + +fun apply(values: List, f: (Map) -> Unit) { + f(null) + println(values) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `type Store struct {`, + `names []string`, + `counts map[string]int`, + `func NewStore(names []string, counts map[string]int) *Store`, + `func apply(values []string, f func(map[string]int))`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoListAndMapLiterals(t *testing.T) { + src := ` +package demo + +fun main() { + val names: List = listOf("alice", "bob") + val ages: Map = mapOf("alice", 30, "bob", 25) + println(names) + println(ages) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `var names []string = gotlinAutoThrow([]string{"alice", "bob"})`, + `var ages map[string]int = gotlinAutoThrow(map[string]int{"alice": 30, "bob": 25})`, + `fmt.Println(names)`, + `fmt.Println(ages)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoListAndMapLiteralsWithCallTypeArgs(t *testing.T) { + src := ` +package demo + +fun main() { + val test = listOf() + val labels = mapOf() + println(test) + println(labels) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `test := gotlinAutoThrow([]int{})`, + `labels := gotlinAutoThrow(map[string]int{})`, + `fmt.Println(test)`, + `fmt.Println(labels)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoUnusedLocalDoesNotEmitNamedVariable(t *testing.T) { + src := ` +package demo + +fun main() { + val test = listOf(1, 7) + println("ok") +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `_ = gotlinAutoThrow([]int{1, 7})`, + `fmt.Println("ok")`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } + if strings.Contains(code, `test :=`) || strings.Contains(code, `var test `) { + t.Fatalf("generated Go should not contain named unused local:\n%s", code) + } +} + +func TestGenerateGoGoStatement(t *testing.T) { + src := ` +package demo + +import fmt + +fun runWorker(name: String) { + fmt.Println(name) +} + +fun main() { + go runWorker("alice") +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `func runWorker(name string)`, + `fmt.Println(name)`, + `go func() {`, + `runWorker("alice")`, + `println("async panic:", recovered)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoForbidsWorkerSelfCalls(t *testing.T) { + src := ` +package demo + +worker Counter { + val n = 0 + fun inc() { + inc() + } +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + _, err = GenerateGo(prog) + if err == nil { + t.Fatal("expected worker self-call generation error") + } + if !strings.Contains(err.Error(), "worker self-calls are forbidden") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGenerateGoWorkerSyncPanicPropagationScaffolding(t *testing.T) { + src := ` +package demo + +worker Counter { + fun boom() { + throw "x" + } +} +` + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + code := string(out) + for _, want := range []string{ + `panicCh := make(chan any, 1)`, + `if recovered := recover(); recovered != nil {`, + `panicCh <- recovered`, + `case recovered := <-panicCh:`, + `panic(recovered)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestParseGoStatementRequiresCall(t *testing.T) { + src := ` +package demo + +fun main() { + go 123 +} +` + + _, err := Parse(src) + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), "'go' expects a function call expression") { + t.Fatalf("unexpected parse error: %v", err) + } +} + +func TestGenerateGoChannelsAndSelect(t *testing.T) { + src := ` +package demo + +fun writer(ch: Channel) { + ch.send(7) +} + +fun main() { + val ch = Channel() + go writer(ch) + select { + ch -> println(it) + } + val v = ch.read() + println(v) +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `func writer(ch chan int)`, + `ch <- 7`, + `ch := gotlinAutoThrow(make(chan int))`, + `go func() {`, + `writer(ch)`, + `select {`, + `case it := <-ch:`, + `fmt.Println(it)`, + `v := gotlinAutoThrow(<-ch)`, + `fmt.Println(v)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoSelectAfterAndEvery(t *testing.T) { + src := ` +package demo + +fun main() { + select { + after(1000) -> println(it) + every(250) -> println("tick") + } +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `"time"`, + `func gotlinEveryMs(ms int) <-chan time.Time {`, + `case it := <-time.After(time.Duration(1000) * time.Millisecond):`, + `fmt.Println(it)`, + `case <-gotlinEveryMs(250):`, + `fmt.Println("tick")`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} + +func TestGenerateGoEveryRejectsNonPositiveMilliseconds(t *testing.T) { + cases := []string{ + `every(0)`, + `every(-1)`, + } + for _, timerCall := range cases { + src := ` +package demo + +fun main() { + select { + ` + timerCall + ` -> println("x") + } +} +` + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + _, err = GenerateGo(prog) + if err == nil { + t.Fatalf("expected go generation error for %s", timerCall) + } + if !strings.Contains(err.Error(), "every(ms) requires ms > 0") { + t.Fatalf("unexpected error for %s: %v", timerCall, err) + } + } +} + +func TestGenerateGoAfterEveryArityValidation(t *testing.T) { + cases := []struct { + call string + want string + }{ + {`after()`, "after(ms) expects exactly one Int argument"}, + {`after(1, 2)`, "after(ms) expects exactly one Int argument"}, + {`every()`, "every(ms) expects exactly one Int argument"}, + {`every(1, 2)`, "every(ms) expects exactly one Int argument"}, + } + for _, tc := range cases { + src := ` +package demo + +fun main() { + select { + ` + tc.call + ` -> println("x") + } +} +` + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + _, err = GenerateGo(prog) + if err == nil { + t.Fatalf("expected go generation error for %s", tc.call) + } + if !strings.Contains(err.Error(), tc.want) { + t.Fatalf("unexpected error for %s: %v", tc.call, err) + } + } +} + +func TestGenerateGoAfterEveryIntValidation(t *testing.T) { + cases := []struct { + call string + want string + }{ + {`after("1s")`, "after(ms) expects an Int argument"}, + {`every(true)`, "every(ms) expects an Int argument"}, + } + for _, tc := range cases { + src := ` +package demo + +fun main() { + select { + ` + tc.call + ` -> println("x") + } +} +` + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + _, err = GenerateGo(prog) + if err == nil { + t.Fatalf("expected go generation error for %s", tc.call) + } + if !strings.Contains(err.Error(), tc.want) { + t.Fatalf("unexpected error for %s: %v", tc.call, err) + } + } +} + +func TestGenerateGoNoTimeImportWhenUnused(t *testing.T) { + src := ` +package demo + +fun main() { + println("ok") +} +` + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + code := string(out) + if strings.Contains(code, `"time"`) { + t.Fatalf("unexpected time import in generated code:\n%s", code) + } +} + +func TestGenerateGoNoTickerShorthandRewrite(t *testing.T) { + src := ` +package demo + +import time + +fun main() { + val tick = time.NewTicker(time.Second) + select { + tick -> println("x") + } +} +` + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + code := string(out) + if strings.Contains(code, "<-tick.C") { + t.Fatalf("ticker shorthand rewrite should be removed:\n%s", code) + } + if !strings.Contains(code, "case <-tick:") { + t.Fatalf("expected raw source receive in generated code:\n%s", code) + } +} + +func TestGenerateGoAutoThrowForBunStyleCalls(t *testing.T) { + src := ` +package demo + +import context +import github.com.uptrace.bun + +class Repo(val db: *bun.DB) { + fun run() { + val ctx = context.Background() + db.NewSelect().ColumnExpr("1").Scan(ctx) + val total = db.NewSelect().ColumnExpr("1").Count(ctx) + println(total) + } +} +` + + prog, err := Parse(src) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + out, err := GenerateGo(prog) + if err != nil { + t.Fatalf("go generation failed: %v", err) + } + + code := string(out) + for _, want := range []string{ + `self.db.NewSelect().ColumnExpr("1").Scan(ctx)`, + `total := gotlinAutoThrow(self.db.NewSelect().ColumnExpr("1").Count(ctx))`, + `fmt.Println(total)`, + } { + if !strings.Contains(code, want) { + t.Fatalf("generated Go missing %q:\n%s", want, code) + } + } +} diff --git a/internal/lang/generate_go.go b/internal/lang/generate_go.go new file mode 100644 index 0000000..6541834 --- /dev/null +++ b/internal/lang/generate_go.go @@ -0,0 +1,2507 @@ +package lang + +import ( + "bytes" + "fmt" + "go/format" + "regexp" + "strconv" + "strings" +) + +func GenerateGo(program *Program) ([]byte, error) { + return generateGo(program, "") +} + +func GenerateGoMain(program *Program) ([]byte, error) { + return generateGo(program, "main") +} + +func generateGo(program *Program, packageOverride string) ([]byte, error) { + var g goGenerator + if err := g.program(program, packageOverride); err != nil { + return nil, err + } + src := g.buf.Bytes() + formatted, err := format.Source(src) + if err != nil { + return src, fmt.Errorf("generated invalid Go: %w", err) + } + return formatted, nil +} + +type goGenerator struct { + buf bytes.Buffer + indentLevel int + needsFmt bool + needsRunCatch bool + needsAutoThrow bool + needsTime bool + needsEveryMs bool + functions map[string]FunctionDecl + classes map[string]ClassDecl + workers map[string]WorkerDecl + currentFunc FunctionDecl + currentClass *ClassDecl + currentWorker *WorkerDecl + currentWorkerFieldReceiver string + scopes []map[string]bool +} + +func (g *goGenerator) program(program *Program, packageOverride string) error { + g.functions = make(map[string]FunctionDecl, len(program.Functions)) + for _, fn := range program.Functions { + g.functions[fn.Name] = fn + } + g.classes = make(map[string]ClassDecl, len(program.Classes)) + for _, class := range program.Classes { + g.classes[class.Name] = class + } + g.workers = make(map[string]WorkerDecl, len(program.Workers)) + for _, worker := range program.Workers { + g.workers[worker.Name] = worker + } + + packageName := goPackageName(program.PackagePath) + if packageOverride != "" { + packageName = packageOverride + } + g.line("package " + packageName) + g.line("") + + for _, fn := range program.Functions { + if usesPrintln(fn.Body) { + g.needsFmt = true + break + } + } + if !g.needsFmt { + for _, class := range program.Classes { + for _, method := range class.Methods { + if usesPrintln(method.Body) { + g.needsFmt = true + break + } + } + if g.needsFmt { + break + } + } + } + if !g.needsFmt { + for _, worker := range program.Workers { + for _, method := range worker.Methods { + if usesPrintln(method.Body) { + g.needsFmt = true + break + } + } + if g.needsFmt { + break + } + } + } + if !g.needsRunCatch { + for _, fn := range program.Functions { + if usesRunCatching(fn.Body) { + g.needsRunCatch = true + break + } + } + } + if !g.needsAutoThrow { + for _, fn := range program.Functions { + if usesAutoThrow(fn.Body) { + g.needsAutoThrow = true + break + } + } + } + if !g.needsAutoThrow { + for _, class := range program.Classes { + for _, method := range class.Methods { + if usesAutoThrow(method.Body) { + g.needsAutoThrow = true + break + } + } + if g.needsAutoThrow { + break + } + } + } + if !g.needsAutoThrow { + for _, worker := range program.Workers { + for _, method := range worker.Methods { + if usesAutoThrow(method.Body) { + g.needsAutoThrow = true + break + } + } + if g.needsAutoThrow { + break + } + } + } + if !g.needsRunCatch { + for _, class := range program.Classes { + for _, method := range class.Methods { + if usesRunCatching(method.Body) { + g.needsRunCatch = true + break + } + } + if g.needsRunCatch { + break + } + } + } + if !g.needsRunCatch { + for _, worker := range program.Workers { + for _, method := range worker.Methods { + if usesRunCatching(method.Body) { + g.needsRunCatch = true + break + } + } + if g.needsRunCatch { + break + } + } + } + for _, fn := range program.Functions { + if usesTimerBuiltins(fn.Body) { + g.needsTime = true + if usesEveryBuiltin(fn.Body) { + g.needsEveryMs = true + } + break + } + } + if !g.needsTime { + for _, class := range program.Classes { + for _, method := range class.Methods { + if usesTimerBuiltins(method.Body) { + g.needsTime = true + if usesEveryBuiltin(method.Body) { + g.needsEveryMs = true + } + break + } + if usesEveryBuiltin(method.Body) { + g.needsEveryMs = true + } + } + if g.needsTime { + break + } + } + } + if !g.needsTime { + for _, worker := range program.Workers { + for _, method := range worker.Methods { + if usesTimerBuiltins(method.Body) { + g.needsTime = true + if usesEveryBuiltin(method.Body) { + g.needsEveryMs = true + } + break + } + if usesEveryBuiltin(method.Body) { + g.needsEveryMs = true + } + } + if g.needsTime { + break + } + } + } + if !g.needsEveryMs { + for _, fn := range program.Functions { + if usesEveryBuiltin(fn.Body) { + g.needsEveryMs = true + break + } + } + } + imports := collectImports(program, g.needsFmt, g.needsTime) + if len(imports) == 1 { + g.line("import " + imports[0]) + g.line("") + } else if len(imports) > 1 { + g.line("import (") + g.indentLevel++ + for _, imp := range imports { + g.line(imp) + } + g.indentLevel-- + g.line(")") + g.line("") + } + + if g.needsRunCatch { + g.emitRunCatchingSupport() + g.line("") + } + if g.needsAutoThrow { + g.emitAutoThrowSupport() + g.line("") + } + if g.needsEveryMs { + g.emitEveryMsSupport() + g.line("") + } + + emitted := false + for _, decl := range program.Interfaces { + if emitted { + g.line("") + } + g.interfaceDecl(decl) + emitted = true + } + for _, class := range program.Classes { + if emitted { + g.line("") + } + if err := g.classDecl(class); err != nil { + return err + } + emitted = true + } + for _, worker := range program.Workers { + if emitted { + g.line("") + } + if err := g.workerDecl(worker); err != nil { + return err + } + emitted = true + } + for _, fn := range program.Functions { + if emitted { + g.line("") + } + if err := g.function(fn); err != nil { + return err + } + emitted = true + } + return nil +} + +func (g *goGenerator) function(fn FunctionDecl) error { + g.currentClass = nil + g.currentWorker = nil + g.currentFunc = fn + g.scopes = nil + g.pushScope() + for _, param := range fn.Params { + g.define(param.Name) + } + g.write("func ") + g.write(fn.Name) + g.write(renderGoParams(fn.Params)) + if ret := mapGoReturnType(fn.ReturnType); ret != "" { + g.write(" ") + g.write(ret) + } + g.write(" {\n") + g.indentLevel++ + if err := g.block(fn.Body); err != nil { + return err + } + g.indentLevel-- + g.line("}") + g.popScope() + return nil +} + +func (g *goGenerator) interfaceDecl(decl InterfaceDecl) { + g.line("type " + decl.Name + " interface {") + g.indentLevel++ + for _, method := range decl.Methods { + g.line(method.Name + renderGoParams(method.Params) + renderGoReturnSuffix(method.ReturnType)) + } + g.indentLevel-- + g.line("}") +} + +func (g *goGenerator) classDecl(class ClassDecl) error { + g.line("type " + class.Name + " struct {") + g.indentLevel++ + for _, field := range class.Fields { + g.line(field.Name + " " + mapGoType(field.Type)) + } + g.indentLevel-- + g.line("}") + g.line("") + g.write("func New") + g.write(class.Name) + g.write("(") + for i, field := range class.Fields { + if i > 0 { + g.write(", ") + } + g.write(field.Name) + g.write(" ") + g.write(mapGoType(field.Type)) + } + g.write(") *") + g.write(class.Name) + g.write(" {\n") + g.indentLevel++ + g.writeIndent() + g.write("return &") + g.write(class.Name) + g.write("{") + for i, field := range class.Fields { + if i > 0 { + g.write(", ") + } + g.write(field.Name) + g.write(": ") + g.write(field.Name) + } + g.write("}\n") + g.indentLevel-- + g.line("}") + + for _, method := range class.Methods { + g.line("") + if err := g.method(class, method); err != nil { + return err + } + } + return nil +} + +func (g *goGenerator) workerDecl(worker WorkerDecl) error { + stateName := worker.Name + "State" + g.line("type " + worker.Name + " struct {") + g.indentLevel++ + g.line("inbox chan func(*" + stateName + ")") + g.indentLevel-- + g.line("}") + g.line("") + g.line("type " + stateName + " struct {") + g.indentLevel++ + for _, field := range worker.Fields { + typ := field.Type + if typ == "" { + switch field.Value.(type) { + case IntExpr: + typ = "Int" + case StringExpr: + typ = "String" + case BoolExpr: + typ = "Boolean" + default: + typ = "any" + } + } + g.line(field.Name + " " + mapGoType(typ)) + } + g.indentLevel-- + g.line("}") + g.line("") + g.write("func New") + g.write(worker.Name) + g.write("() *") + g.write(worker.Name) + g.write(" {\n") + g.indentLevel++ + g.line("self := &" + worker.Name + "{") + g.indentLevel++ + g.line("inbox: make(chan func(*" + stateName + ")),") + g.indentLevel-- + g.line("}") + g.line("state := &" + stateName + "{}") + for _, field := range worker.Fields { + value, err := g.expr(field.Value, field.Type) + if err != nil { + return err + } + value = g.autoThrowValue(field.Value, value) + g.line("state." + field.Name + " = " + value) + } + g.line("go func() {") + g.indentLevel++ + g.line("for fn := range self.inbox {") + g.indentLevel++ + g.line("fn(state)") + g.indentLevel-- + g.line("}") + g.indentLevel-- + g.line("}()") + g.line("return self") + g.indentLevel-- + g.line("}") + + for _, method := range worker.Methods { + g.line("") + if err := g.workerMethod(worker, method); err != nil { + return err + } + } + return nil +} + +func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error { + g.currentClass = &class + g.currentFunc = fn + g.scopes = nil + g.pushScope() + g.define("self") + g.define("this") + for _, param := range fn.Params { + g.define(param.Name) + } + + g.write("func (self *") + g.write(class.Name) + g.write(") ") + g.write(fn.Name) + g.write(renderGoParams(fn.Params)) + if ret := mapGoReturnType(fn.ReturnType); ret != "" { + g.write(" ") + g.write(ret) + } + g.write(" {\n") + g.indentLevel++ + if err := g.block(fn.Body); err != nil { + return err + } + g.indentLevel-- + g.line("}") + g.popScope() + g.currentClass = nil + return nil +} + +func (g *goGenerator) workerMethod(worker WorkerDecl, fn FunctionDecl) error { + g.currentClass = nil + g.currentWorker = &worker + g.currentWorkerFieldReceiver = "state" + g.currentFunc = fn + g.scopes = nil + g.pushScope() + g.define("self") + g.define("this") + for _, param := range fn.Params { + g.define(param.Name) + } + + g.write("func (self *") + g.write(worker.Name) + g.write(") ") + g.write(fn.Name) + g.write(renderGoParams(fn.Params)) + if ret := mapGoReturnType(fn.ReturnType); ret != "" { + g.write(" ") + g.write(ret) + } + g.write(" {\n") + g.indentLevel++ + stateName := worker.Name + "State" + retType := mapGoReturnType(fn.ReturnType) + if retType == "" { + g.line("done := make(chan struct{}, 1)") + g.line("panicCh := make(chan any, 1)") + g.line("self.inbox <- func(state *" + stateName + ") {") + g.indentLevel++ + g.line("defer func() {") + g.indentLevel++ + g.line("if recovered := recover(); recovered != nil {") + g.indentLevel++ + g.line("panicCh <- recovered") + g.indentLevel-- + g.line("}") + g.line("done <- struct{}{}") + g.indentLevel-- + g.line("}()") + if err := g.block(fn.Body); err != nil { + return err + } + g.indentLevel-- + g.line("}") + g.line("<-done") + g.line("select {") + g.indentLevel++ + g.line("case recovered := <-panicCh:") + g.indentLevel++ + g.line("panic(recovered)") + g.indentLevel-- + g.line("default:") + g.indentLevel-- + g.line("}") + } else { + g.line("type workerResult struct {") + g.indentLevel++ + g.line("value " + retType) + g.line("recovered any") + g.indentLevel-- + g.line("}") + g.line("reply := make(chan workerResult, 1)") + g.line("self.inbox <- func(state *" + stateName + ") {") + g.indentLevel++ + g.line("result := workerResult{}") + g.line("defer func() {") + g.indentLevel++ + g.line("if recovered := recover(); recovered != nil {") + g.indentLevel++ + g.line("result.recovered = recovered") + g.indentLevel-- + g.line("}") + g.line("reply <- result") + g.indentLevel-- + g.line("}()") + g.writeIndent() + g.write("result.value = func()") + g.write(renderGoReturnSuffix(fn.ReturnType)) + g.write(" {\n") + g.indentLevel++ + if err := g.block(fn.Body); err != nil { + return err + } + g.indentLevel-- + g.line("}()") + g.indentLevel-- + g.line("}") + g.line("result := <-reply") + g.line("if result.recovered != nil {") + g.indentLevel++ + g.line("panic(result.recovered)") + g.indentLevel-- + g.line("}") + g.line("return result.value") + } + g.indentLevel-- + g.line("}") + g.popScope() + g.currentWorker = nil + g.currentWorkerFieldReceiver = "" + return nil +} + +func (g *goGenerator) block(stmts []Stmt) error { + for i, stmt := range stmts { + if err := g.stmt(stmt, stmts[i+1:]); err != nil { + return err + } + } + return nil +} + +func (g *goGenerator) stmt(stmt Stmt, tail []Stmt) error { + switch s := stmt.(type) { + case VarDecl: + value, err := g.expr(s.Value, s.Type) + if err != nil { + return err + } + value = g.autoThrowValue(s.Value, value) + if !nameUsedInStmts(s.Name, tail) { + g.line(fmt.Sprintf("_ = %s", value)) + return nil + } + if s.Type == "" { + g.line(fmt.Sprintf("%s := %s", s.Name, value)) + } else { + g.line(fmt.Sprintf("var %s %s = %s", s.Name, mapGoType(s.Type), value)) + } + g.define(s.Name) + case MultiVarDecl: + value, err := g.expr(s.Value, "") + if err != nil { + return err + } + assigned := make([]string, len(s.Names)) + usedCount := 0 + for i, name := range s.Names { + if nameUsedInStmts(name, tail) { + assigned[i] = name + usedCount++ + } else { + assigned[i] = "_" + } + } + if usedCount == 0 { + g.line(fmt.Sprintf("%s = %s", strings.Join(assigned, ", "), value)) + return nil + } + g.line(fmt.Sprintf("%s := %s", strings.Join(assigned, ", "), value)) + for i, name := range s.Names { + if assigned[i] != "_" { + g.define(name) + } + } + case AssignStmt: + value, err := g.expr(s.Value, "") + if err != nil { + return err + } + value = g.autoThrowValue(s.Value, value) + g.line(fmt.Sprintf("%s = %s", g.assignTarget(s.Name), value)) + case AddAssignStmt: + value, err := g.expr(s.Value, "") + if err != nil { + return err + } + value = g.autoThrowValue(s.Value, value) + g.line(fmt.Sprintf("%s += %s", g.assignTarget(s.Name), value)) + case MultiAssignStmt: + value, err := g.expr(s.Value, "") + if err != nil { + return err + } + names := make([]string, 0, len(s.Names)) + for _, name := range s.Names { + names = append(names, g.assignTarget(name)) + } + g.line(fmt.Sprintf("%s = %s", strings.Join(names, ", "), value)) + case ReturnStmt: + if s.Value == nil { + g.line("return") + } else { + value, err := g.expr(s.Value, g.currentFunc.ReturnType) + if err != nil { + return err + } + value = g.autoThrowValue(s.Value, value) + g.line(fmt.Sprintf("return %s", value)) + } + case ThrowStmt: + value, err := g.expr(s.Value, "") + if err != nil { + return err + } + g.line(fmt.Sprintf("panic(%s)", value)) + case GoStmt: + value, err := g.expr(s.Value, "") + if err != nil { + return err + } + g.line("go func() {") + g.indentLevel++ + g.line("defer func() {") + g.indentLevel++ + g.line("if recovered := recover(); recovered != nil {") + g.indentLevel++ + g.line(`println("async panic:", recovered)`) + g.indentLevel-- + g.line("}") + g.indentLevel-- + g.line("}()") + g.line(value) + g.indentLevel-- + g.line("}()") + case ExprStmt: + value, err := g.expr(s.Value, "") + if err != nil { + return err + } + g.line(value) + case IfStmt: + cond, err := g.expr(s.Cond, "") + if err != nil { + return err + } + g.writeIndent() + g.write(fmt.Sprintf("if %s {\n", cond)) + g.indentLevel++ + g.pushScope() + if err := g.block(s.Then); err != nil { + return err + } + g.popScope() + g.indentLevel-- + g.writeIndent() + g.write("}") + if len(s.Else) > 0 { + g.write(" else {\n") + g.indentLevel++ + g.pushScope() + if err := g.block(s.Else); err != nil { + return err + } + g.popScope() + g.indentLevel-- + g.writeIndent() + g.write("}") + } + g.write("\n") + case WhileStmt: + cond, err := g.expr(s.Cond, "") + if err != nil { + return err + } + g.writeIndent() + g.write(fmt.Sprintf("for %s {\n", cond)) + g.indentLevel++ + g.pushScope() + if err := g.block(s.Body); err != nil { + return err + } + g.popScope() + g.indentLevel-- + g.line("}") + case SelectStmt: + g.line("select {") + g.indentLevel++ + for _, c := range s.Cases { + source, err := g.selectReceiveChannelExpr(c.Source) + if err != nil { + return err + } + caseUsesIt := nameUsedInStmts("it", c.Body) + g.writeIndent() + if caseUsesIt { + g.write(fmt.Sprintf("case it := <-%s:\n", source)) + } else { + g.write(fmt.Sprintf("case <-%s:\n", source)) + } + g.indentLevel++ + g.pushScope() + if caseUsesIt { + g.define("it") + } + if err := g.block(c.Body); err != nil { + return err + } + g.popScope() + g.indentLevel-- + } + g.indentLevel-- + g.line("}") + case TryCatchStmt: + g.writeIndent() + g.write("func() {\n") + g.indentLevel++ + g.writeIndent() + g.write("defer func() {\n") + g.indentLevel++ + g.writeIndent() + g.write("if recovered := recover(); recovered != nil {\n") + g.indentLevel++ + g.pushScope() + g.define(s.CatchName) + g.line(fmt.Sprintf("%s := recovered", s.CatchName)) + if err := g.block(s.CatchBody); err != nil { + return err + } + g.popScope() + g.indentLevel-- + g.line("}") + g.indentLevel-- + g.line("}()") + g.pushScope() + if err := g.block(s.TryBody); err != nil { + return err + } + g.popScope() + g.indentLevel-- + g.line("}()") + default: + return fmt.Errorf("unsupported statement %T", stmt) + } + return nil +} + +func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { + switch e := expr.(type) { + case IdentExpr: + if g.currentClass != nil { + if e.Name == "this" { + return "self", nil + } + if g.classField(*g.currentClass, e.Name) && !g.isDefined(e.Name) { + return "self." + e.Name, nil + } + } + if g.currentWorker != nil { + if e.Name == "this" { + return "self", nil + } + if g.workerField(*g.currentWorker, e.Name) && !g.isDefined(e.Name) { + receiver := g.currentWorkerFieldReceiver + if receiver == "" { + receiver = "self" + } + return receiver + "." + e.Name, nil + } + } + return e.Name, nil + case IntExpr: + return e.Value, nil + case StringExpr: + return e.Value, nil + case BoolExpr: + if e.Value { + return "true", nil + } + return "false", nil + case NullExpr: + return "nil", nil + case UnaryExpr: + value, err := g.wrapExpr(e.Value, "") + if err != nil { + return "", err + } + return e.Op + value, nil + case BinaryExpr: + left, err := g.wrapExpr(e.Left, "") + if err != nil { + return "", err + } + right, err := g.wrapExpr(e.Right, "") + if err != nil { + return "", err + } + return fmt.Sprintf("%s %s %s", left, e.Op, right), nil + case CallExpr: + if g.currentWorker != nil && g.isWorkerSelfCall(e.Callee) { + return "", fmt.Errorf("worker self-calls are forbidden") + } + if ident, ok := e.Callee.(IdentExpr); ok { + switch ident.Name { + case "listOf", "mutableListOf": + return g.listLiteral(e.Args, e.TypeArgs, expectedType) + case "mapOf", "mutableMapOf": + return g.mapLiteral(e.Args, e.TypeArgs, expectedType) + case "Channel": + return g.channelMakeExpr(e.Args, e.TypeArgs, expectedType) + case "after", "every": + if lowered, handled, err := g.lowerTimerSourceCall(e); handled || err != nil { + return lowered, err + } + } + } + if selector, ok := e.Callee.(SelectorExpr); ok { + switch selector.Name { + case "read": + if len(e.Args) != 0 { + return "", fmt.Errorf("read() expects no arguments") + } + receiver, err := g.expr(selector.Receiver, "") + if err != nil { + return "", err + } + return "<-" + receiver, nil + case "send": + if len(e.Args) != 1 { + return "", fmt.Errorf("send(value) expects exactly one argument") + } + receiver, err := g.expr(selector.Receiver, "") + if err != nil { + return "", err + } + arg, err := g.expr(e.Args[0], "") + if err != nil { + return "", err + } + return receiver + " <- " + arg, nil + } + } + + args := make([]string, 0, len(e.Args)) + argTypes := g.callArgTypes(e.Callee, len(e.Args)) + for i, arg := range e.Args { + argType := "" + if i < len(argTypes) { + argType = argTypes[i] + } + value, err := g.expr(arg, argType) + if err != nil { + return "", err + } + args = append(args, value) + } + if ident, ok := e.Callee.(IdentExpr); ok && ident.Name == "println" { + return fmt.Sprintf("fmt.Println(%s)", strings.Join(args, ", ")), nil + } + if ident, ok := e.Callee.(IdentExpr); ok && ident.Name == "runCatching" { + if len(args) != 1 { + return "", fmt.Errorf("runCatching expects exactly one argument") + } + return fmt.Sprintf("gotlinRunCatching(%s)", args[0]), nil + } + if ident, ok := e.Callee.(IdentExpr); ok { + if _, ok := g.classes[ident.Name]; ok { + return fmt.Sprintf("New%s(%s)", ident.Name, strings.Join(args, ", ")), nil + } + if _, ok := g.workers[ident.Name]; ok { + if len(args) != 0 { + return "", fmt.Errorf("worker constructor %s does not accept arguments", ident.Name) + } + return fmt.Sprintf("New%s()", ident.Name), nil + } + } + callee, err := g.expr(e.Callee, "") + if err != nil { + return "", err + } + return fmt.Sprintf("%s(%s)", callee, strings.Join(args, ", ")), nil + case SelectorExpr: + receiver, err := g.expr(e.Receiver, "") + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%s", receiver, e.Name), nil + case LambdaExpr: + return g.lambda(e, expectedType) + default: + return "", fmt.Errorf("unsupported expression %T", expr) + } +} + +func (g *goGenerator) listLiteral(args []Expr, typeArgs []string, expectedType string) (string, error) { + elemType := "" + if len(typeArgs) > 0 { + if len(typeArgs) != 1 { + return "", fmt.Errorf("listOf expects exactly one type argument") + } + elemType = typeArgs[0] + } + if base, genericArgs, ok := parseGenericType(expectedType); ok && (base == "List" || base == "MutableList") && len(genericArgs) == 1 { + if elemType == "" { + elemType = genericArgs[0] + } + } + goElemType := "any" + if elemType != "" { + goElemType = mapGoType(elemType) + } + + values := make([]string, 0, len(args)) + for _, arg := range args { + value, err := g.expr(arg, elemType) + if err != nil { + return "", err + } + values = append(values, value) + } + return "[]" + goElemType + "{" + strings.Join(values, ", ") + "}", nil +} + +func (g *goGenerator) mapLiteral(args []Expr, typeArgs []string, expectedType string) (string, error) { + if len(args)%2 != 0 { + return "", fmt.Errorf("mapOf expects an even number of arguments (key/value pairs)") + } + + keyType := "" + valType := "" + if len(typeArgs) > 0 { + if len(typeArgs) != 2 { + return "", fmt.Errorf("mapOf expects exactly two type arguments") + } + keyType = typeArgs[0] + valType = typeArgs[1] + } + if base, genericArgs, ok := parseGenericType(expectedType); ok && (base == "Map" || base == "MutableMap") && len(genericArgs) == 2 { + if keyType == "" { + keyType = genericArgs[0] + } + if valType == "" { + valType = genericArgs[1] + } + } + goKeyType := "any" + goValType := "any" + if keyType != "" { + goKeyType = mapGoType(keyType) + } + if valType != "" { + goValType = mapGoType(valType) + } + + var entries []string + for i := 0; i < len(args); i += 2 { + key, err := g.expr(args[i], keyType) + if err != nil { + return "", err + } + val, err := g.expr(args[i+1], valType) + if err != nil { + return "", err + } + entries = append(entries, key+": "+val) + } + return "map[" + goKeyType + "]" + goValType + "{" + strings.Join(entries, ", ") + "}", nil +} + +func (g *goGenerator) channelMakeExpr(args []Expr, typeArgs []string, expectedType string) (string, error) { + elemType := "" + if len(typeArgs) > 0 { + if len(typeArgs) != 1 { + return "", fmt.Errorf("Channel expects exactly one type argument") + } + elemType = typeArgs[0] + } + if elemType == "" { + if base, genericArgs, ok := parseGenericType(expectedType); ok && base == "Channel" && len(genericArgs) == 1 { + elemType = genericArgs[0] + } + } + goElemType := "any" + if elemType != "" { + goElemType = mapGoType(elemType) + } + + if len(args) > 1 { + return "", fmt.Errorf("Channel() expects at most one capacity argument") + } + if len(args) == 1 { + capacity, err := g.expr(args[0], "Int") + if err != nil { + return "", err + } + return "make(chan " + goElemType + ", " + capacity + ")", nil + } + return "make(chan " + goElemType + ")", nil +} + +func (g *goGenerator) lowerTimerSourceCall(call CallExpr) (string, bool, error) { + ident, ok := call.Callee.(IdentExpr) + if !ok { + return "", false, nil + } + if ident.Name != "after" && ident.Name != "every" { + return "", false, nil + } + if len(call.TypeArgs) > 0 { + return "", true, fmt.Errorf("%s does not accept type arguments", ident.Name) + } + if len(call.Args) != 1 { + return "", true, fmt.Errorf("%s(ms) expects exactly one Int argument", ident.Name) + } + msExpr := call.Args[0] + switch msExpr.(type) { + case StringExpr, BoolExpr, NullExpr: + return "", true, fmt.Errorf("%s(ms) expects an Int argument", ident.Name) + } + msValue, err := g.expr(msExpr, "Int") + if err != nil { + return "", true, err + } + if ident.Name == "every" { + if parsed, ok := staticIntValue(msExpr); ok && parsed <= 0 { + return "", true, fmt.Errorf("every(ms) requires ms > 0") + } + } + if ident.Name == "after" { + return "time.After(time.Duration(" + msValue + ") * time.Millisecond)", true, nil + } + g.needsEveryMs = true + return "gotlinEveryMs(" + msValue + ")", true, nil +} + +func (g *goGenerator) selectReceiveChannelExpr(source Expr) (string, error) { + if call, ok := source.(CallExpr); ok { + if lowered, handled, err := g.lowerTimerSourceCall(call); handled || err != nil { + return lowered, err + } + } + return g.expr(source, "") +} + +func (g *goGenerator) wrapExpr(expr Expr, expectedType string) (string, error) { + switch expr.(type) { + case BinaryExpr: + value, err := g.expr(expr, expectedType) + if err != nil { + return "", err + } + return "(" + value + ")", nil + default: + return g.expr(expr, expectedType) + } +} + +func (g *goGenerator) line(text string) { + g.writeIndent() + g.write(text) + g.write("\n") +} + +func (g *goGenerator) writeIndent() { + g.write(strings.Repeat("\t", g.indentLevel)) +} + +func (g *goGenerator) write(text string) { + g.buf.WriteString(text) +} + +func mapGoType(name string) string { + if params, ret, ok := parseFunctionType(name); ok { + var goParams []string + for _, param := range params { + goParams = append(goParams, mapGoType(param)) + } + goRet := mapGoType(ret) + if goRet == "" { + return "func(" + strings.Join(goParams, ", ") + ")" + } + return "func(" + strings.Join(goParams, ", ") + ") " + goRet + } + + if base, args, ok := parseGenericType(name); ok { + mapped := make([]string, 0, len(args)) + for _, arg := range args { + mapped = append(mapped, mapGoType(arg)) + } + switch base { + case "List", "MutableList": + if len(mapped) == 1 { + return "[]" + mapped[0] + } + case "Map", "MutableMap": + if len(mapped) == 2 { + return "map[" + mapped[0] + "]" + mapped[1] + } + case "Channel": + if len(mapped) == 1 { + return "chan " + mapped[0] + } + } + return base + "[" + strings.Join(mapped, ", ") + "]" + } + + switch name { + case "Int": + return "int" + case "String": + return "string" + case "Boolean": + return "bool" + case "Unit": + return "" + default: + return name + } +} + +func mapGoReturnType(name string) string { + return mapGoType(name) +} + +func usesPrintln(stmts []Stmt) bool { + for _, stmt := range stmts { + switch s := stmt.(type) { + case ExprStmt: + if call, ok := s.Value.(CallExpr); ok && isBuiltinPrintln(call.Callee) { + return true + } + if exprUsesPrintln(s.Value) { + return true + } + case VarDecl: + if exprUsesPrintln(s.Value) { + return true + } + case AssignStmt: + if exprUsesPrintln(s.Value) { + return true + } + case AddAssignStmt: + if exprUsesPrintln(s.Value) { + return true + } + case MultiAssignStmt: + if exprUsesPrintln(s.Value) { + return true + } + case MultiVarDecl: + if exprUsesPrintln(s.Value) { + return true + } + case ReturnStmt: + if s.Value != nil && exprUsesPrintln(s.Value) { + return true + } + case ThrowStmt: + if exprUsesPrintln(s.Value) { + return true + } + case GoStmt: + if exprUsesPrintln(s.Value) { + return true + } + case IfStmt: + if exprUsesPrintln(s.Cond) || usesPrintln(s.Then) || usesPrintln(s.Else) { + return true + } + case WhileStmt: + if exprUsesPrintln(s.Cond) || usesPrintln(s.Body) { + return true + } + case SelectStmt: + for _, c := range s.Cases { + if exprUsesPrintln(c.Source) || usesPrintln(c.Body) { + return true + } + } + case TryCatchStmt: + if usesPrintln(s.TryBody) || usesPrintln(s.CatchBody) { + return true + } + } + } + return false +} + +func usesRunCatching(stmts []Stmt) bool { + for _, stmt := range stmts { + switch s := stmt.(type) { + case VarDecl: + if exprUsesRunCatching(s.Value) { + return true + } + case MultiVarDecl: + if exprUsesRunCatching(s.Value) { + return true + } + case AssignStmt: + if exprUsesRunCatching(s.Value) { + return true + } + case AddAssignStmt: + if exprUsesRunCatching(s.Value) { + return true + } + case MultiAssignStmt: + if exprUsesRunCatching(s.Value) { + return true + } + case ReturnStmt: + if s.Value != nil && exprUsesRunCatching(s.Value) { + return true + } + case ThrowStmt: + if exprUsesRunCatching(s.Value) { + return true + } + case GoStmt: + if exprUsesRunCatching(s.Value) { + return true + } + case ExprStmt: + if exprUsesRunCatching(s.Value) { + return true + } + case IfStmt: + if exprUsesRunCatching(s.Cond) || usesRunCatching(s.Then) || usesRunCatching(s.Else) { + return true + } + case WhileStmt: + if exprUsesRunCatching(s.Cond) || usesRunCatching(s.Body) { + return true + } + case SelectStmt: + for _, c := range s.Cases { + if exprUsesRunCatching(c.Source) || usesRunCatching(c.Body) { + return true + } + } + case TryCatchStmt: + if usesRunCatching(s.TryBody) || usesRunCatching(s.CatchBody) { + return true + } + } + } + return false +} + +func usesAutoThrow(stmts []Stmt) bool { + for _, stmt := range stmts { + switch s := stmt.(type) { + case VarDecl: + if _, ok := s.Value.(CallExpr); ok { + return true + } + case AssignStmt: + if _, ok := s.Value.(CallExpr); ok { + return true + } + case AddAssignStmt: + if _, ok := s.Value.(CallExpr); ok { + return true + } + case ReturnStmt: + if s.Value != nil { + if _, ok := s.Value.(CallExpr); ok { + return true + } + } + case IfStmt: + if usesAutoThrow(s.Then) || usesAutoThrow(s.Else) { + return true + } + case WhileStmt: + if usesAutoThrow(s.Body) { + return true + } + case SelectStmt: + for _, c := range s.Cases { + if usesAutoThrow(c.Body) { + return true + } + } + case TryCatchStmt: + if usesAutoThrow(s.TryBody) || usesAutoThrow(s.CatchBody) { + return true + } + } + } + return false +} + +func usesTimerBuiltins(stmts []Stmt) bool { + for _, stmt := range stmts { + switch s := stmt.(type) { + case VarDecl: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case MultiVarDecl: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case AssignStmt: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case AddAssignStmt: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case MultiAssignStmt: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case ReturnStmt: + if s.Value != nil && exprUsesTimerBuiltins(s.Value) { + return true + } + case ThrowStmt: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case GoStmt: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case ExprStmt: + if exprUsesTimerBuiltins(s.Value) { + return true + } + case IfStmt: + if exprUsesTimerBuiltins(s.Cond) || usesTimerBuiltins(s.Then) || usesTimerBuiltins(s.Else) { + return true + } + case WhileStmt: + if exprUsesTimerBuiltins(s.Cond) || usesTimerBuiltins(s.Body) { + return true + } + case SelectStmt: + for _, c := range s.Cases { + if exprUsesTimerBuiltins(c.Source) || usesTimerBuiltins(c.Body) { + return true + } + } + case TryCatchStmt: + if usesTimerBuiltins(s.TryBody) || usesTimerBuiltins(s.CatchBody) { + return true + } + } + } + return false +} + +func usesEveryBuiltin(stmts []Stmt) bool { + for _, stmt := range stmts { + switch s := stmt.(type) { + case VarDecl: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case MultiVarDecl: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case AssignStmt: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case AddAssignStmt: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case MultiAssignStmt: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case ReturnStmt: + if s.Value != nil && exprUsesEveryBuiltin(s.Value) { + return true + } + case ThrowStmt: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case GoStmt: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case ExprStmt: + if exprUsesEveryBuiltin(s.Value) { + return true + } + case IfStmt: + if exprUsesEveryBuiltin(s.Cond) || usesEveryBuiltin(s.Then) || usesEveryBuiltin(s.Else) { + return true + } + case WhileStmt: + if exprUsesEveryBuiltin(s.Cond) || usesEveryBuiltin(s.Body) { + return true + } + case SelectStmt: + for _, c := range s.Cases { + if exprUsesEveryBuiltin(c.Source) || usesEveryBuiltin(c.Body) { + return true + } + } + case TryCatchStmt: + if usesEveryBuiltin(s.TryBody) || usesEveryBuiltin(s.CatchBody) { + return true + } + } + } + return false +} + +func (g *goGenerator) lambda(lambda LambdaExpr, expectedType string) (string, error) { + params := lambda.Params + returnType := "" + paramTypes := []string(nil) + + if lambda.ImplicitIt { + expectedReturnType := "" + ok := false + paramTypes, expectedReturnType, ok = parseFunctionType(expectedType) + if !ok { + return "", fmt.Errorf("lambda with implicit it requires a function type context") + } + if len(paramTypes) == 1 { + params = []Param{{Name: "it", Type: paramTypes[0]}} + } else if len(paramTypes) == 0 { + params = nil + } else { + params = make([]Param, 0, len(paramTypes)) + for _, typ := range paramTypes { + params = append(params, Param{Name: "_", Type: typ}) + } + } + returnType = expectedReturnType + } else if parsedParamTypes, expectedReturnType, ok := parseFunctionType(expectedType); ok && len(parsedParamTypes) == len(params) { + paramTypes = parsedParamTypes + returnType = expectedReturnType + } + + if returnType == "" && lambdaHasValueReturn(lambda.Body) { + return "", fmt.Errorf("lambda with value return requires a function type context") + } + + for i := range params { + if params[i].Type == "" { + if i >= len(paramTypes) || paramTypes[i] == "" { + return "", fmt.Errorf("lambda parameter %q requires a type or typed call context", params[i].Name) + } + params[i].Type = paramTypes[i] + } + } + + var b strings.Builder + b.WriteString("func(") + for i, param := range params { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(param.Name) + b.WriteString(" ") + b.WriteString(mapGoType(param.Type)) + } + b.WriteString(")") + if goRet := mapGoType(returnType); goRet != "" { + b.WriteString(" ") + b.WriteString(goRet) + } + b.WriteString(" {\n") + + sub := goGenerator{ + indentLevel: 1, + needsFmt: g.needsFmt, + functions: g.functions, + classes: g.classes, + currentFunc: FunctionDecl{ReturnType: returnType}, + currentClass: g.currentClass, + } + if g.currentClass != nil { + classCopy := *g.currentClass + sub.currentClass = &classCopy + } + sub.scopes = g.cloneScopes() + sub.pushScope() + if lambda.ImplicitIt { + sub.define("it") + } + for _, param := range params { + sub.define(param.Name) + } + if err := sub.block(lambda.Body); err != nil { + return "", err + } + b.Write(sub.buf.Bytes()) + b.WriteString("}") + return b.String(), nil +} + +func (g *goGenerator) callArgTypes(callee Expr, argCount int) []string { + ident, ok := callee.(IdentExpr) + if ok { + if ident.Name == "runCatching" { + return []string{"() -> Unit"} + } + fn, ok := g.functions[ident.Name] + if ok { + argTypes := make([]string, 0, min(argCount, len(fn.Params))) + for i := 0; i < argCount && i < len(fn.Params); i++ { + argTypes = append(argTypes, fn.Params[i].Type) + } + return argTypes + } + class, ok := g.classes[ident.Name] + if ok { + argTypes := make([]string, 0, min(argCount, len(class.Fields))) + for i := 0; i < argCount && i < len(class.Fields); i++ { + argTypes = append(argTypes, class.Fields[i].Type) + } + return argTypes + } + return nil + } + + selector, ok := selectorPath(callee) + if !ok { + return nil + } + switch selector { + case "http.HandleFunc": + return []string{"String", "(http.ResponseWriter, *http.Request) -> Unit"} + default: + return nil + } +} + +func nameUsedInStmts(name string, stmts []Stmt) bool { + return nameUsedInStmtsWithShadow(name, stmts, false) +} + +func nameUsedInStmtsWithShadow(name string, stmts []Stmt, shadowed bool) bool { + localShadowed := shadowed + for _, stmt := range stmts { + switch s := stmt.(type) { + case VarDecl: + if exprUsesName(s.Value, name, localShadowed) { + return true + } + if s.Name == name { + localShadowed = true + } + case MultiVarDecl: + if exprUsesName(s.Value, name, localShadowed) { + return true + } + for _, declared := range s.Names { + if declared == name { + localShadowed = true + break + } + } + case AssignStmt: + if !localShadowed && s.Name == name { + return true + } + if exprUsesName(s.Value, name, localShadowed) { + return true + } + case AddAssignStmt: + if !localShadowed && s.Name == name { + return true + } + if exprUsesName(s.Value, name, localShadowed) { + return true + } + case MultiAssignStmt: + if !localShadowed { + for _, assigned := range s.Names { + if assigned == name { + return true + } + } + } + if exprUsesName(s.Value, name, localShadowed) { + return true + } + case ReturnStmt: + if s.Value != nil && exprUsesName(s.Value, name, localShadowed) { + return true + } + case ThrowStmt: + if exprUsesName(s.Value, name, localShadowed) { + return true + } + case GoStmt: + if exprUsesName(s.Value, name, localShadowed) { + return true + } + case ExprStmt: + if exprUsesName(s.Value, name, localShadowed) { + return true + } + case IfStmt: + if exprUsesName(s.Cond, name, localShadowed) { + return true + } + if nameUsedInStmtsWithShadow(name, s.Then, localShadowed) { + return true + } + if nameUsedInStmtsWithShadow(name, s.Else, localShadowed) { + return true + } + case WhileStmt: + if exprUsesName(s.Cond, name, localShadowed) { + return true + } + if nameUsedInStmtsWithShadow(name, s.Body, localShadowed) { + return true + } + case SelectStmt: + for _, c := range s.Cases { + if exprUsesName(c.Source, name, localShadowed) { + return true + } + if nameUsedInStmtsWithShadow(name, c.Body, localShadowed) { + return true + } + } + case TryCatchStmt: + if nameUsedInStmtsWithShadow(name, s.TryBody, localShadowed) { + return true + } + catchShadowed := localShadowed || s.CatchName == name + if nameUsedInStmtsWithShadow(name, s.CatchBody, catchShadowed) { + return true + } + } + } + return false +} + +func exprUsesName(expr Expr, name string, shadowed bool) bool { + switch e := expr.(type) { + case IdentExpr: + return !shadowed && e.Name == name + case UnaryExpr: + return exprUsesName(e.Value, name, shadowed) + case BinaryExpr: + return exprUsesName(e.Left, name, shadowed) || exprUsesName(e.Right, name, shadowed) + case CallExpr: + if exprUsesName(e.Callee, name, shadowed) { + return true + } + for _, arg := range e.Args { + if exprUsesName(arg, name, shadowed) { + return true + } + } + return false + case SelectorExpr: + return exprUsesName(e.Receiver, name, shadowed) + case LambdaExpr: + lambdaShadowed := shadowed + if e.ImplicitIt && name == "it" { + lambdaShadowed = true + } + for _, param := range e.Params { + if param.Name == name { + lambdaShadowed = true + break + } + } + return nameUsedInStmtsWithShadow(name, e.Body, lambdaShadowed) + default: + return false + } +} + +func (g *goGenerator) pushScope() { + g.scopes = append(g.scopes, map[string]bool{}) +} + +func (g *goGenerator) popScope() { + if len(g.scopes) == 0 { + return + } + g.scopes = g.scopes[:len(g.scopes)-1] +} + +func (g *goGenerator) define(name string) { + if len(g.scopes) == 0 { + g.pushScope() + } + g.scopes[len(g.scopes)-1][name] = true +} + +func (g *goGenerator) isDefined(name string) bool { + for i := len(g.scopes) - 1; i >= 0; i-- { + if g.scopes[i][name] { + return true + } + } + 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) workerField(worker WorkerDecl, name string) bool { + for _, field := range worker.Fields { + if field.Name == name { + return true + } + } + return false +} + +func (g *goGenerator) workerHasMethod(worker WorkerDecl, name string) bool { + for _, method := range worker.Methods { + if method.Name == name { + return true + } + } + return false +} + +func (g *goGenerator) isWorkerSelfCall(callee Expr) bool { + if g.currentWorker == nil { + return false + } + worker := *g.currentWorker + switch c := callee.(type) { + case IdentExpr: + if !g.workerHasMethod(worker, c.Name) { + return false + } + return !g.isDefined(c.Name) + case SelectorExpr: + if !g.workerHasMethod(worker, c.Name) { + return false + } + recv, ok := c.Receiver.(IdentExpr) + if !ok { + return false + } + return recv.Name == "self" || recv.Name == "this" + default: + return false + } +} + +func (g *goGenerator) assignTarget(name string) string { + if g.currentClass != nil && g.classField(*g.currentClass, name) && !g.isDefined(name) { + return "self." + name + } + if g.currentWorker != nil && g.workerField(*g.currentWorker, name) && !g.isDefined(name) { + receiver := g.currentWorkerFieldReceiver + if receiver == "" { + receiver = "self" + } + return receiver + "." + name + } + return name +} + +func (g *goGenerator) cloneScopes() []map[string]bool { + dup := make([]map[string]bool, 0, len(g.scopes)) + for _, scope := range g.scopes { + copyScope := make(map[string]bool, len(scope)) + for k, v := range scope { + copyScope[k] = v + } + dup = append(dup, copyScope) + } + return dup +} + +func renderGoParams(params []Param) string { + var b strings.Builder + b.WriteString("(") + for i, param := range params { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(param.Name) + b.WriteString(" ") + b.WriteString(mapGoType(param.Type)) + } + b.WriteString(")") + return b.String() +} + +func renderGoReturnSuffix(returnType string) string { + if ret := mapGoReturnType(returnType); ret != "" { + return " " + ret + } + return "" +} + +func exprUsesPrintln(expr Expr) bool { + switch e := expr.(type) { + case CallExpr: + if isBuiltinPrintln(e.Callee) { + return true + } + for _, arg := range e.Args { + if exprUsesPrintln(arg) { + return true + } + } + case UnaryExpr: + return exprUsesPrintln(e.Value) + case BinaryExpr: + return exprUsesPrintln(e.Left) || exprUsesPrintln(e.Right) + case SelectorExpr: + return exprUsesPrintln(e.Receiver) + case LambdaExpr: + return usesPrintln(e.Body) + } + return false +} + +func exprUsesRunCatching(expr Expr) bool { + switch e := expr.(type) { + case CallExpr: + if ident, ok := e.Callee.(IdentExpr); ok && ident.Name == "runCatching" { + return true + } + if exprUsesRunCatching(e.Callee) { + return true + } + for _, arg := range e.Args { + if exprUsesRunCatching(arg) { + return true + } + } + case UnaryExpr: + return exprUsesRunCatching(e.Value) + case BinaryExpr: + return exprUsesRunCatching(e.Left) || exprUsesRunCatching(e.Right) + case SelectorExpr: + return exprUsesRunCatching(e.Receiver) + case LambdaExpr: + return usesRunCatching(e.Body) + } + return false +} + +func exprUsesTimerBuiltins(expr Expr) bool { + switch e := expr.(type) { + case CallExpr: + if ident, ok := e.Callee.(IdentExpr); ok && (ident.Name == "after" || ident.Name == "every") { + return true + } + if exprUsesTimerBuiltins(e.Callee) { + return true + } + for _, arg := range e.Args { + if exprUsesTimerBuiltins(arg) { + return true + } + } + case UnaryExpr: + return exprUsesTimerBuiltins(e.Value) + case BinaryExpr: + return exprUsesTimerBuiltins(e.Left) || exprUsesTimerBuiltins(e.Right) + case SelectorExpr: + return exprUsesTimerBuiltins(e.Receiver) + case LambdaExpr: + return usesTimerBuiltins(e.Body) + } + return false +} + +func exprUsesEveryBuiltin(expr Expr) bool { + switch e := expr.(type) { + case CallExpr: + if ident, ok := e.Callee.(IdentExpr); ok && ident.Name == "every" { + return true + } + if exprUsesEveryBuiltin(e.Callee) { + return true + } + for _, arg := range e.Args { + if exprUsesEveryBuiltin(arg) { + return true + } + } + case UnaryExpr: + return exprUsesEveryBuiltin(e.Value) + case BinaryExpr: + return exprUsesEveryBuiltin(e.Left) || exprUsesEveryBuiltin(e.Right) + case SelectorExpr: + return exprUsesEveryBuiltin(e.Receiver) + case LambdaExpr: + return usesEveryBuiltin(e.Body) + } + return false +} + +func isBuiltinPrintln(expr Expr) bool { + ident, ok := expr.(IdentExpr) + return ok && ident.Name == "println" +} + +func collectImports(program *Program, needsFmt bool, needsTime bool) []string { + seen := map[string]bool{} + var imports []string + usedAliases := usedImportAliases(program) + + if needsFmt { + seen[`"fmt"`] = true + imports = append(imports, `"fmt"`) + } + if needsTime { + seen[`"time"`] = true + imports = append(imports, `"time"`) + } + for _, imp := range program.Imports { + path := imp.Path + goPath := path + if !strings.HasPrefix(path, `"`) { + goPath = importPathToGoPath(path) + path = `"` + goPath + `"` + } + rendered := path + if imp.Alias != "" { + rendered = imp.Alias + " " + path + } else if !usedAliases[defaultImportAlias(imp)] { + rendered = "_ " + path + } + if seen[rendered] { + continue + } + seen[rendered] = true + imports = append(imports, rendered) + } + + return imports +} + +func goPackageName(packagePath string) string { + if packagePath == "" { + return "main" + } + parts := strings.Split(packagePath, ".") + return parts[len(parts)-1] +} + +func usedImportAliases(program *Program) map[string]bool { + used := map[string]bool{} + markType := func(typ string) { + for _, alias := range typeImportAliases(typ) { + used[alias] = true + } + } + var walkExpr func(expr Expr) + var walkStmt func(stmt Stmt) + walkExpr = func(expr Expr) { + switch e := expr.(type) { + case IdentExpr: + used[e.Name] = true + case UnaryExpr: + walkExpr(e.Value) + case BinaryExpr: + walkExpr(e.Left) + walkExpr(e.Right) + case CallExpr: + walkExpr(e.Callee) + for _, arg := range e.Args { + walkExpr(arg) + } + case SelectorExpr: + if alias, ok := selectorRootAlias(e); ok { + used[alias] = true + } + walkExpr(e.Receiver) + case LambdaExpr: + for _, param := range e.Params { + markType(param.Type) + } + for _, stmt := range e.Body { + walkStmt(stmt) + } + } + } + walkStmt = func(stmt Stmt) { + switch s := stmt.(type) { + case VarDecl: + markType(s.Type) + walkExpr(s.Value) + case MultiVarDecl: + walkExpr(s.Value) + case AssignStmt: + walkExpr(s.Value) + case AddAssignStmt: + walkExpr(s.Value) + case MultiAssignStmt: + walkExpr(s.Value) + case ReturnStmt: + if s.Value != nil { + walkExpr(s.Value) + } + case ThrowStmt: + walkExpr(s.Value) + case GoStmt: + walkExpr(s.Value) + case ExprStmt: + walkExpr(s.Value) + case IfStmt: + walkExpr(s.Cond) + for _, inner := range s.Then { + walkStmt(inner) + } + for _, inner := range s.Else { + walkStmt(inner) + } + case WhileStmt: + walkExpr(s.Cond) + for _, inner := range s.Body { + walkStmt(inner) + } + case SelectStmt: + for _, c := range s.Cases { + walkExpr(c.Source) + for _, inner := range c.Body { + walkStmt(inner) + } + } + case TryCatchStmt: + for _, inner := range s.TryBody { + walkStmt(inner) + } + for _, inner := range s.CatchBody { + walkStmt(inner) + } + } + } + + for _, decl := range program.Interfaces { + for _, method := range decl.Methods { + for _, param := range method.Params { + markType(param.Type) + } + markType(method.ReturnType) + } + } + for _, decl := range program.Classes { + for _, field := range decl.Fields { + markType(field.Type) + } + for _, method := range decl.Methods { + for _, param := range method.Params { + markType(param.Type) + } + markType(method.ReturnType) + for _, stmt := range method.Body { + walkStmt(stmt) + } + } + } + for _, decl := range program.Workers { + for _, field := range decl.Fields { + markType(field.Type) + walkExpr(field.Value) + } + for _, method := range decl.Methods { + for _, param := range method.Params { + markType(param.Type) + } + markType(method.ReturnType) + for _, stmt := range method.Body { + walkStmt(stmt) + } + } + } + for _, fn := range program.Functions { + for _, param := range fn.Params { + markType(param.Type) + } + markType(fn.ReturnType) + for _, stmt := range fn.Body { + walkStmt(stmt) + } + } + return used +} + +func selectorRootAlias(expr SelectorExpr) (string, bool) { + switch r := expr.Receiver.(type) { + case IdentExpr: + return r.Name, true + case SelectorExpr: + return selectorRootAlias(r) + default: + return "", false + } +} + +func typeImportAliases(typ string) []string { + if typ == "" { + return nil + } + matches := regexp.MustCompile(`([A-Za-z_][A-Za-z0-9_]*)\.`).FindAllStringSubmatch(typ, -1) + out := make([]string, 0, len(matches)) + for _, m := range matches { + if len(m) == 2 { + out = append(out, m[1]) + } + } + return out +} + +func defaultImportAlias(imp ImportDecl) string { + if imp.Alias != "" { + return imp.Alias + } + path := imp.Path + if strings.HasPrefix(path, `"`) { + path = strings.Trim(path, `"`) + parts := strings.Split(path, "/") + return parts[len(parts)-1] + } + parts := strings.Split(path, ".") + return parts[len(parts)-1] +} + +func importPathToGoPath(path string) string { + trimmed := strings.TrimPrefix(path, "go.") + parts := strings.Split(trimmed, ".") + if len(parts) >= 3 && isDomainTLD(parts[1]) { + return parts[0] + "." + parts[1] + "/" + strings.Join(parts[2:], "/") + } + return strings.ReplaceAll(trimmed, ".", "/") +} + +func isDomainTLD(segment string) bool { + switch segment { + case "com", "org", "net", "io", "dev", "app", "ai": + return true + default: + return false + } +} + +func selectorPath(expr Expr) (string, bool) { + switch e := expr.(type) { + case IdentExpr: + return e.Name, true + case SelectorExpr: + left, ok := selectorPath(e.Receiver) + if !ok { + return "", false + } + return left + "." + e.Name, true + default: + return "", false + } +} + +func (g *goGenerator) emitRunCatchingSupport() { + g.line("type gotlinResult struct {") + g.indentLevel++ + g.line("exception any") + g.indentLevel-- + g.line("}") + g.line("") + g.line("func gotlinRunCatching(fn func()) (result gotlinResult) {") + g.indentLevel++ + g.line("defer func() {") + g.indentLevel++ + g.line("if recovered := recover(); recovered != nil {") + g.indentLevel++ + g.line("result.exception = recovered") + g.indentLevel-- + g.line("}") + g.indentLevel-- + g.line("}()") + g.line("fn()") + g.line("return") + g.indentLevel-- + g.line("}") + g.line("") + g.line("func (r gotlinResult) isSuccess() bool {") + g.indentLevel++ + g.line("return r.exception == nil") + g.indentLevel-- + g.line("}") + g.line("") + g.line("func (r gotlinResult) exceptionOrNull() any {") + g.indentLevel++ + g.line("return r.exception") + g.indentLevel-- + g.line("}") +} + +func (g *goGenerator) emitAutoThrowSupport() { + g.line("func gotlinAutoThrow[T any](value T, rest ...any) T {") + g.indentLevel++ + g.line("if len(rest) == 1 {") + g.indentLevel++ + g.line("if rest[0] == nil {") + g.indentLevel++ + g.line("return value") + g.indentLevel-- + g.line("}") + g.line("if err, ok := rest[0].(error); ok {") + g.indentLevel++ + g.line("if err != nil {") + g.indentLevel++ + g.line("panic(err)") + g.indentLevel-- + g.line("}") + g.line("return value") + g.indentLevel-- + g.line("}") + g.indentLevel-- + g.line("}") + g.line("if len(rest) == 0 {") + g.indentLevel++ + g.line("if any(value) == nil {") + g.indentLevel++ + g.line("return value") + g.indentLevel-- + g.line("}") + g.line("if err, ok := any(value).(error); ok {") + g.indentLevel++ + g.line("if err != nil {") + g.indentLevel++ + g.line("panic(err)") + g.indentLevel-- + g.line("}") + g.line("var zero T") + g.line("return zero") + g.indentLevel-- + g.line("}") + g.line("return value") + g.indentLevel-- + g.line("}") + g.line(`panic("multi-value Go call requires explicit destructuring unless second value is error")`) + g.indentLevel-- + g.line("}") +} + +func (g *goGenerator) emitEveryMsSupport() { + g.line("func gotlinEveryMs(ms int) <-chan time.Time {") + g.indentLevel++ + g.line("if ms <= 0 {") + g.indentLevel++ + g.line(`panic("every(ms) requires ms > 0")`) + g.indentLevel-- + g.line("}") + g.line("ticker := time.NewTicker(time.Duration(ms) * time.Millisecond)") + g.line("out := make(chan time.Time)") + g.line("go func() {") + g.indentLevel++ + g.line("for value := range ticker.C {") + g.indentLevel++ + g.line("out <- value") + g.indentLevel-- + g.line("}") + g.indentLevel-- + g.line("}()") + g.line("return out") + g.indentLevel-- + g.line("}") +} + +func (g *goGenerator) autoThrowValue(original Expr, rendered string) string { + if _, ok := original.(CallExpr); ok { + return "gotlinAutoThrow(" + rendered + ")" + } + return rendered +} + +func parseIntLiteral(text string) (int, error) { + return strconv.Atoi(text) +} + +func staticIntValue(expr Expr) (int, bool) { + switch e := expr.(type) { + case IntExpr: + parsed, err := parseIntLiteral(e.Value) + if err != nil { + return 0, false + } + return parsed, true + case UnaryExpr: + if e.Op != "-" { + return 0, false + } + inner, ok := staticIntValue(e.Value) + if !ok { + return 0, false + } + return -inner, true + default: + return 0, false + } +} + +func parseFunctionType(text string) ([]string, string, bool) { + text = strings.TrimSpace(text) + if !strings.HasPrefix(text, "(") { + return nil, "", false + } + + depth := 0 + end := -1 + for i, r := range text { + switch r { + case '(': + depth++ + case ')': + depth-- + if depth == 0 { + end = i + goto done + } + } + } + +done: + if end == -1 { + return nil, "", false + } + rest := strings.TrimSpace(text[end+1:]) + if !strings.HasPrefix(rest, "->") { + return nil, "", false + } + + paramsText := strings.TrimSpace(text[1:end]) + retText := strings.TrimSpace(strings.TrimPrefix(rest, "->")) + var params []string + if paramsText != "" { + params = splitTopLevel(paramsText, ',') + } + return params, retText, true +} + +func splitTopLevel(text string, sep rune) []string { + var parts []string + depthParen := 0 + depthAngle := 0 + start := 0 + for i, r := range text { + switch r { + case '(': + depthParen++ + case ')': + depthParen-- + case '<': + depthAngle++ + case '>': + if depthAngle > 0 { + depthAngle-- + } + default: + if r == sep && depthParen == 0 && depthAngle == 0 { + parts = append(parts, strings.TrimSpace(text[start:i])) + start = i + 1 + } + } + } + parts = append(parts, strings.TrimSpace(text[start:])) + return parts +} + +func parseGenericType(text string) (string, []string, bool) { + text = strings.TrimSpace(text) + start := strings.Index(text, "<") + if start <= 0 || !strings.HasSuffix(text, ">") { + return "", nil, false + } + + depth := 0 + end := -1 + for i, r := range text { + switch r { + case '<': + depth++ + case '>': + depth-- + if depth == 0 { + end = i + } + } + } + if end != len(text)-1 || depth != 0 { + return "", nil, false + } + + base := strings.TrimSpace(text[:start]) + argsText := strings.TrimSpace(text[start+1 : end]) + if base == "" || argsText == "" { + return "", nil, false + } + return base, splitTopLevel(argsText, ','), true +} + +func lambdaHasValueReturn(stmts []Stmt) bool { + for _, stmt := range stmts { + switch s := stmt.(type) { + case ReturnStmt: + if s.Value != nil { + return true + } + case IfStmt: + if lambdaHasValueReturn(s.Then) || lambdaHasValueReturn(s.Else) { + return true + } + case WhileStmt: + if lambdaHasValueReturn(s.Body) { + return true + } + case SelectStmt: + for _, c := range s.Cases { + if lambdaHasValueReturn(c.Body) { + return true + } + } + } + } + return false +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/internal/lang/lexer.go b/internal/lang/lexer.go new file mode 100644 index 0000000..5e5e47b --- /dev/null +++ b/internal/lang/lexer.go @@ -0,0 +1,167 @@ +package lang + +import ( + "fmt" + "unicode" +) + +type lexer struct { + src []rune + pos int +} + +func lex(input string) ([]token, error) { + l := &lexer{src: []rune(input)} + var tokens []token + for { + tok, err := l.next() + if err != nil { + return nil, err + } + tokens = append(tokens, tok) + if tok.kind == tokenEOF { + return tokens, nil + } + } +} + +func (l *lexer) next() (token, error) { + l.skipWhitespace() + start := l.pos + if l.pos >= len(l.src) { + return token{kind: tokenEOF, pos: start}, nil + } + + ch := l.src[l.pos] + switch { + case isIdentStart(ch): + l.pos++ + for l.pos < len(l.src) && isIdentPart(l.src[l.pos]) { + l.pos++ + } + lexeme := string(l.src[start:l.pos]) + if kind, ok := keywords[lexeme]; ok { + return token{kind: kind, lexeme: lexeme, pos: start}, nil + } + return token{kind: tokenIdent, lexeme: lexeme, pos: start}, nil + case unicode.IsDigit(ch): + l.pos++ + for l.pos < len(l.src) && unicode.IsDigit(l.src[l.pos]) { + l.pos++ + } + return token{kind: tokenInt, lexeme: string(l.src[start:l.pos]), pos: start}, nil + case ch == '"': + l.pos++ + for l.pos < len(l.src) && l.src[l.pos] != '"' { + if l.src[l.pos] == '\\' { + l.pos++ + if l.pos >= len(l.src) { + return token{}, fmt.Errorf("unterminated string at %d", start) + } + } + l.pos++ + } + if l.pos >= len(l.src) { + return token{}, fmt.Errorf("unterminated string at %d", start) + } + l.pos++ + return token{kind: tokenString, lexeme: string(l.src[start:l.pos]), pos: start}, nil + default: + l.pos++ + switch ch { + case '(': + return token{kind: tokenLParen, lexeme: "(", pos: start}, nil + case ')': + return token{kind: tokenRParen, lexeme: ")", pos: start}, nil + case '{': + return token{kind: tokenLBrace, lexeme: "{", pos: start}, nil + case '}': + return token{kind: tokenRBrace, lexeme: "}", pos: start}, nil + case ',': + return token{kind: tokenComma, lexeme: ",", pos: start}, nil + case '.': + return token{kind: tokenDot, lexeme: ".", pos: start}, nil + case ':': + return token{kind: tokenColon, lexeme: ":", pos: start}, nil + case ';': + return token{kind: tokenSemicolon, lexeme: ";", pos: start}, nil + case '+': + if l.match('=') { + return token{kind: tokenPlusAssign, lexeme: "+=", pos: start}, nil + } + return token{kind: tokenPlus, lexeme: "+", pos: start}, nil + case '-': + if l.match('>') { + return token{kind: tokenArrow, lexeme: "->", pos: start}, nil + } + return token{kind: tokenMinus, lexeme: "-", pos: start}, nil + case '*': + return token{kind: tokenStar, lexeme: "*", pos: start}, nil + case '/': + if l.match('/') { + for l.pos < len(l.src) && l.src[l.pos] != '\n' { + l.pos++ + } + return l.next() + } + return token{kind: tokenSlash, lexeme: "/", pos: start}, nil + case '%': + return token{kind: tokenPercent, lexeme: "%", pos: start}, nil + case '!': + if l.match('=') { + return token{kind: tokenNeq, lexeme: "!=", pos: start}, nil + } + return token{kind: tokenBang, lexeme: "!", pos: start}, nil + case '=': + if l.match('=') { + return token{kind: tokenEq, lexeme: "==", pos: start}, nil + } + return token{kind: tokenAssign, lexeme: "=", pos: start}, nil + case '<': + if l.match('=') { + return token{kind: tokenLte, lexeme: "<=", pos: start}, nil + } + return token{kind: tokenLt, lexeme: "<", pos: start}, nil + case '>': + if l.match('=') { + return token{kind: tokenGte, lexeme: ">=", pos: start}, nil + } + return token{kind: tokenGt, lexeme: ">", pos: start}, nil + case '&': + if l.match('&') { + return token{kind: tokenAnd, lexeme: "&&", pos: start}, nil + } + case '|': + if l.match('|') { + return token{kind: tokenOr, lexeme: "||", pos: start}, nil + } + } + return token{}, fmt.Errorf("unexpected character %q at %d", ch, start) + } +} + +func (l *lexer) match(expected rune) bool { + if l.pos >= len(l.src) || l.src[l.pos] != expected { + return false + } + l.pos++ + return true +} + +func (l *lexer) skipWhitespace() { + for l.pos < len(l.src) { + if unicode.IsSpace(l.src[l.pos]) { + l.pos++ + continue + } + break + } +} + +func isIdentStart(ch rune) bool { + return unicode.IsLetter(ch) || ch == '_' +} + +func isIdentPart(ch rune) bool { + return isIdentStart(ch) || unicode.IsDigit(ch) +} diff --git a/internal/lang/parser.go b/internal/lang/parser.go new file mode 100644 index 0000000..942f3ed --- /dev/null +++ b/internal/lang/parser.go @@ -0,0 +1,1056 @@ +package lang + +import ( + "fmt" + "strings" +) + +type parser struct { + tokens []token + pos int +} + +func Parse(input string) (*Program, error) { + tokens, err := lex(input) + if err != nil { + return nil, err + } + p := &parser{tokens: tokens} + return p.parseProgram() +} + +func (p *parser) parseProgram() (*Program, error) { + prog := &Program{} + for !p.check(tokenEOF) { + switch { + case p.match(tokenPackage): + if prog.PackagePath != "" { + return nil, fmt.Errorf("duplicate package declaration") + } + packagePath, err := p.parsePackageDecl() + if err != nil { + return nil, err + } + prog.PackagePath = packagePath + case p.match(tokenImport): + imp, err := p.parseImport() + if err != nil { + return nil, err + } + prog.Imports = append(prog.Imports, imp) + case p.check(tokenInterface): + decl, err := p.parseInterface() + if err != nil { + return nil, err + } + prog.Interfaces = append(prog.Interfaces, decl) + case p.check(tokenClass): + decl, err := p.parseClass() + if err != nil { + return nil, err + } + prog.Classes = append(prog.Classes, decl) + case p.check(tokenWorker): + decl, err := p.parseWorker() + if err != nil { + return nil, err + } + prog.Workers = append(prog.Workers, decl) + case p.check(tokenFun): + fn, err := p.parseFunction() + if err != nil { + return nil, err + } + prog.Functions = append(prog.Functions, fn) + default: + tok := p.peek() + return nil, fmt.Errorf("expected top-level declaration at %d, found %q", tok.pos, tok.lexeme) + } + } + return prog, nil +} + +func (p *parser) parsePackageDecl() (string, error) { + path, err := p.parseImportPath() + if err != nil { + return "", err + } + p.match(tokenSemicolon) + return path, nil +} + +func (p *parser) parseImport() (ImportDecl, error) { + if p.check(tokenIdent) && p.peekN(1).kind == tokenString { + alias := p.advance().lexeme + path, err := p.expect(tokenString, "expected import path") + if err != nil { + return ImportDecl{}, err + } + p.match(tokenSemicolon) + return ImportDecl{Alias: alias, Path: path.lexeme}, nil + } + + if p.check(tokenString) { + path, err := p.expect(tokenString, "expected import path") + if err != nil { + return ImportDecl{}, err + } + p.match(tokenSemicolon) + return ImportDecl{Path: path.lexeme}, nil + } + + firstPath, err := p.parseImportPath() + if err != nil { + return ImportDecl{}, err + } + + alias := "" + path := firstPath + if importPathContinues(p.peek()) { + alias = firstPath + path, err = p.parseImportPath() + if err != nil { + return ImportDecl{}, err + } + } + + if err := validateBareImportPath(path); err != nil { + return ImportDecl{}, err + } + + p.match(tokenSemicolon) + return ImportDecl{Alias: alias, Path: path}, nil +} + +func (p *parser) parseInterface() (InterfaceDecl, error) { + if _, err := p.expect(tokenInterface, "expected 'interface'"); err != nil { + return InterfaceDecl{}, err + } + name, err := p.expect(tokenIdent, "expected interface name") + if err != nil { + return InterfaceDecl{}, err + } + if _, err := p.expect(tokenLBrace, "expected '{'"); err != nil { + return InterfaceDecl{}, err + } + + var methods []FunctionSignature + for !p.check(tokenRBrace) && !p.check(tokenEOF) { + method, err := p.parseFunctionSignature() + if err != nil { + return InterfaceDecl{}, err + } + methods = append(methods, method) + p.match(tokenSemicolon) + } + if _, err := p.expect(tokenRBrace, "expected '}'"); err != nil { + return InterfaceDecl{}, err + } + + return InterfaceDecl{Name: name.lexeme, Methods: methods}, nil +} + +func (p *parser) parseClass() (ClassDecl, error) { + if _, err := p.expect(tokenClass, "expected 'class'"); err != nil { + return ClassDecl{}, err + } + name, err := p.expect(tokenIdent, "expected class name") + if err != nil { + return ClassDecl{}, err + } + var fields []FieldDecl + if p.match(tokenLParen) { + fields, err = p.parseClassFields() + if err != nil { + return ClassDecl{}, err + } + if _, err := p.expect(tokenRParen, "expected ')'"); err != nil { + return ClassDecl{}, err + } + } + + parents, err := p.parseClassParents() + if err != nil { + return ClassDecl{}, err + } + if !p.match(tokenLBrace) { + return ClassDecl{Name: name.lexeme, Fields: fields, Parents: parents, Methods: nil}, nil + } + + var methods []FunctionDecl + for !p.check(tokenRBrace) && !p.check(tokenEOF) { + p.match(tokenOverride) + if !p.check(tokenFun) { + tok := p.peek() + return ClassDecl{}, fmt.Errorf("expected class member at %d, found %q", tok.pos, tok.lexeme) + } + method, err := p.parseFunction() + if err != nil { + return ClassDecl{}, err + } + methods = append(methods, method) + p.match(tokenSemicolon) + } + if _, err := p.expect(tokenRBrace, "expected '}'"); err != nil { + return ClassDecl{}, err + } + + return ClassDecl{Name: name.lexeme, Fields: fields, Parents: parents, Methods: methods}, nil +} + +func (p *parser) parseWorker() (WorkerDecl, error) { + if _, err := p.expect(tokenWorker, "expected 'worker'"); err != nil { + return WorkerDecl{}, err + } + name, err := p.expect(tokenIdent, "expected worker name") + if err != nil { + return WorkerDecl{}, err + } + if _, err := p.expect(tokenLBrace, "expected '{'"); err != nil { + return WorkerDecl{}, err + } + + var fields []WorkerFieldDecl + var methods []FunctionDecl + for !p.check(tokenRBrace) && !p.check(tokenEOF) { + switch { + case p.match(tokenVal), p.match(tokenVar): + mutable := p.tokens[p.pos-1].kind == tokenVar + fieldName, err := p.expect(tokenIdent, "expected field name") + if err != nil { + return WorkerDecl{}, err + } + typ := "" + if p.match(tokenColon) { + parsed, err := p.parseTypeRef() + if err != nil { + return WorkerDecl{}, err + } + typ = parsed + } + if _, err := p.expect(tokenAssign, "expected '=' after field declaration"); err != nil { + return WorkerDecl{}, err + } + value, err := p.parseExpr(0) + if err != nil { + return WorkerDecl{}, err + } + fields = append(fields, WorkerFieldDecl{ + Mutable: mutable, + Name: fieldName.lexeme, + Type: typ, + Value: value, + }) + p.match(tokenSemicolon) + case p.check(tokenFun): + method, err := p.parseFunction() + if err != nil { + return WorkerDecl{}, err + } + methods = append(methods, method) + p.match(tokenSemicolon) + default: + tok := p.peek() + return WorkerDecl{}, fmt.Errorf("expected worker member at %d, found %q", tok.pos, tok.lexeme) + } + } + if _, err := p.expect(tokenRBrace, "expected '}'"); err != nil { + return WorkerDecl{}, err + } + return WorkerDecl{Name: name.lexeme, Fields: fields, Methods: methods}, nil +} + +func (p *parser) parseClassParents() ([]string, error) { + if !p.match(tokenColon) { + return nil, nil + } + + var parents []string + for { + parent, err := p.parseTypeRef() + if err != nil { + return nil, err + } + parents = append(parents, parent) + if !p.match(tokenComma) { + return parents, nil + } + } +} + +func (p *parser) parseClassFields() ([]FieldDecl, error) { + var fields []FieldDecl + if p.check(tokenRParen) { + return fields, nil + } + for { + mutable := false + switch { + case p.match(tokenVal): + mutable = false + case p.match(tokenVar): + mutable = true + default: + tok := p.peek() + return nil, fmt.Errorf("expected 'val' or 'var' in class constructor at %d, found %q", tok.pos, tok.lexeme) + } + name, err := p.expect(tokenIdent, "expected field name") + if err != nil { + return nil, err + } + if _, err := p.expect(tokenColon, "expected ':' after field name"); err != nil { + return nil, err + } + typ, err := p.parseTypeRef() + if err != nil { + return nil, err + } + fields = append(fields, FieldDecl{Mutable: mutable, Name: name.lexeme, Type: typ}) + if !p.match(tokenComma) { + return fields, nil + } + } +} + +func (p *parser) parseFunction() (FunctionDecl, error) { + signature, err := p.parseFunctionSignature() + if err != nil { + return FunctionDecl{}, err + } + body, err := p.parseBlock() + if err != nil { + return FunctionDecl{}, err + } + + return FunctionDecl{ + Name: signature.Name, + Params: signature.Params, + ReturnType: signature.ReturnType, + Body: body, + }, nil +} + +func (p *parser) parseFunctionSignature() (FunctionSignature, error) { + if _, err := p.expect(tokenFun, "expected 'fun'"); err != nil { + return FunctionSignature{}, err + } + name, err := p.expect(tokenIdent, "expected function name") + if err != nil { + return FunctionSignature{}, err + } + if _, err := p.expect(tokenLParen, "expected '('"); err != nil { + return FunctionSignature{}, err + } + params, err := p.parseParams() + if err != nil { + return FunctionSignature{}, err + } + if _, err := p.expect(tokenRParen, "expected ')'"); err != nil { + return FunctionSignature{}, err + } + + returnType := "Unit" + if p.match(tokenColon) { + typ, err := p.parseTypeRef() + if err != nil { + return FunctionSignature{}, err + } + returnType = typ + } + + return FunctionSignature{ + Name: name.lexeme, + Params: params, + ReturnType: returnType, + }, nil +} + +func (p *parser) parseParams() ([]Param, error) { + var params []Param + if p.check(tokenRParen) { + return params, nil + } + for { + name, err := p.expect(tokenIdent, "expected parameter name") + if err != nil { + return nil, err + } + if _, err := p.expect(tokenColon, "expected ':' after parameter name"); err != nil { + return nil, err + } + typ, err := p.parseTypeRef() + if err != nil { + return nil, err + } + params = append(params, Param{Name: name.lexeme, Type: typ}) + if !p.match(tokenComma) { + return params, nil + } + } +} + +func (p *parser) parseBlock() ([]Stmt, error) { + if _, err := p.expect(tokenLBrace, "expected '{'"); err != nil { + return nil, err + } + var stmts []Stmt + for !p.check(tokenRBrace) && !p.check(tokenEOF) { + stmt, err := p.parseStmt() + if err != nil { + return nil, err + } + stmts = append(stmts, stmt) + p.match(tokenSemicolon) + } + if _, err := p.expect(tokenRBrace, "expected '}'"); err != nil { + return nil, err + } + return stmts, nil +} + +func (p *parser) parseStmt() (Stmt, error) { + switch { + case p.match(tokenVal): + return p.parseVarDecl(false) + case p.match(tokenVar): + return p.parseVarDecl(true) + case p.match(tokenReturn): + if p.check(tokenSemicolon) || p.check(tokenRBrace) { + return ReturnStmt{}, nil + } + expr, err := p.parseExpr(0) + if err != nil { + return nil, err + } + return ReturnStmt{Value: expr}, nil + case p.match(tokenThrow): + expr, err := p.parseExpr(0) + if err != nil { + return nil, err + } + return ThrowStmt{Value: expr}, nil + case p.match(tokenGo): + var expr Expr + var err error + if p.check(tokenLBrace) { + body, err := p.parseBlock() + if err != nil { + return nil, err + } + expr = CallExpr{Callee: LambdaExpr{Body: body}} + } else { + expr, err = p.parseExpr(0) + if err != nil { + return nil, err + } + } + if _, ok := expr.(CallExpr); !ok { + return nil, fmt.Errorf("'go' expects a function call expression") + } + return GoStmt{Value: expr}, nil + case p.match(tokenIf): + return p.parseIf() + case p.match(tokenWhile): + return p.parseWhile() + case p.match(tokenSelect): + return p.parseSelect() + case p.match(tokenTry): + return p.parseTryCatch() + case p.check(tokenIdent) && (p.peekN(1).kind == tokenAssign || p.peekN(1).kind == tokenComma || p.peekN(1).kind == tokenPlusAssign): + if p.peekN(1).kind == tokenPlusAssign { + name := p.advance() + if _, err := p.expect(tokenPlusAssign, "expected '+=' in assignment"); err != nil { + return nil, err + } + value, err := p.parseExpr(0) + if err != nil { + return nil, err + } + return AddAssignStmt{Name: name.lexeme, Value: value}, nil + } + names, err := p.parseNameList() + if err != nil { + return nil, err + } + if _, err := p.expect(tokenAssign, "expected '=' in assignment"); err != nil { + return nil, err + } + value, err := p.parseExpr(0) + if err != nil { + return nil, err + } + if len(names) == 1 { + return AssignStmt{Name: names[0], Value: value}, nil + } + return MultiAssignStmt{Names: names, Value: value}, nil + default: + expr, err := p.parseExpr(0) + if err != nil { + return nil, err + } + return ExprStmt{Value: expr}, nil + } +} + +func (p *parser) parseSelect() (Stmt, error) { + if _, err := p.expect(tokenLBrace, "expected '{' after select"); err != nil { + return nil, err + } + var cases []SelectCase + for !p.check(tokenRBrace) && !p.check(tokenEOF) { + source, err := p.parseExpr(0) + if err != nil { + return nil, err + } + if _, err := p.expect(tokenArrow, "expected '->' in select case"); err != nil { + return nil, err + } + var body []Stmt + if p.check(tokenLBrace) { + body, err = p.parseBlock() + if err != nil { + return nil, err + } + } else { + stmt, err := p.parseStmt() + if err != nil { + return nil, err + } + body = []Stmt{stmt} + p.match(tokenSemicolon) + } + cases = append(cases, SelectCase{Source: source, Body: body}) + } + if _, err := p.expect(tokenRBrace, "expected '}' after select"); err != nil { + return nil, err + } + return SelectStmt{Cases: cases}, nil +} + +func (p *parser) parseTryCatch() (Stmt, error) { + tryBody, err := p.parseBlock() + if err != nil { + return nil, err + } + if _, err := p.expect(tokenCatch, "expected 'catch' after try block"); err != nil { + return nil, err + } + if _, err := p.expect(tokenLParen, "expected '(' after catch"); err != nil { + return nil, err + } + name, err := p.expect(tokenIdent, "expected catch variable name") + if err != nil { + return nil, err + } + catchType := "" + if p.match(tokenColon) { + typ, err := p.parseTypeRef() + if err != nil { + return nil, err + } + catchType = typ + } + if _, err := p.expect(tokenRParen, "expected ')' after catch clause"); err != nil { + return nil, err + } + catchBody, err := p.parseBlock() + if err != nil { + return nil, err + } + return TryCatchStmt{ + TryBody: tryBody, + CatchName: name.lexeme, + CatchType: catchType, + CatchBody: catchBody, + }, nil +} + +func (p *parser) parseVarDecl(mutable bool) (Stmt, error) { + names, err := p.parseNameList() + if err != nil { + return nil, err + } + name := names[0] + var typ string + if p.match(tokenColon) { + if len(names) > 1 { + return nil, fmt.Errorf("multi-variable declaration does not support explicit type annotations") + } + typeRef, err := p.parseTypeRef() + if err != nil { + return nil, err + } + typ = typeRef + } + if _, err := p.expect(tokenAssign, "expected '=' in declaration"); err != nil { + return nil, err + } + value, err := p.parseExpr(0) + if err != nil { + return nil, err + } + if len(names) == 1 { + return VarDecl{Mutable: mutable, Name: name, Type: typ, Value: value}, nil + } + return MultiVarDecl{Mutable: mutable, Names: names, Value: value}, nil +} + +func (p *parser) parseIf() (Stmt, error) { + if _, err := p.expect(tokenLParen, "expected '(' after if"); err != nil { + return nil, err + } + cond, err := p.parseExpr(0) + if err != nil { + return nil, err + } + if _, err := p.expect(tokenRParen, "expected ')' after condition"); err != nil { + return nil, err + } + thenBlock, err := p.parseBlock() + if err != nil { + return nil, err + } + var elseBlock []Stmt + if p.match(tokenElse) { + elseBlock, err = p.parseBlock() + if err != nil { + return nil, err + } + } + return IfStmt{Cond: cond, Then: thenBlock, Else: elseBlock}, nil +} + +func (p *parser) parseWhile() (Stmt, error) { + if _, err := p.expect(tokenLParen, "expected '(' after while"); err != nil { + return nil, err + } + cond, err := p.parseExpr(0) + if err != nil { + return nil, err + } + if _, err := p.expect(tokenRParen, "expected ')' after condition"); err != nil { + return nil, err + } + body, err := p.parseBlock() + if err != nil { + return nil, err + } + return WhileStmt{Cond: cond, Body: body}, nil +} + +func (p *parser) parseExpr(minPrec int) (Expr, error) { + left, err := p.parsePrefix() + if err != nil { + return nil, err + } + for { + op := p.peek() + prec, ok := infixPrecedence(op.kind) + if !ok || prec < minPrec { + return left, nil + } + p.advance() + right, err := p.parseExpr(prec + 1) + if err != nil { + return nil, err + } + left = BinaryExpr{Left: left, Op: op.lexeme, Right: right} + } +} + +func (p *parser) parsePrefix() (Expr, error) { + tok := p.advance() + switch tok.kind { + case tokenIdent: + return p.parsePostfix(IdentExpr{Name: tok.lexeme}) + case tokenInt: + return IntExpr{Value: tok.lexeme}, nil + case tokenString: + return StringExpr{Value: tok.lexeme}, nil + case tokenTrue: + return BoolExpr{Value: true}, nil + case tokenFalse: + return BoolExpr{Value: false}, nil + case tokenNull: + return NullExpr{}, nil + case tokenLParen: + expr, err := p.parseExpr(0) + if err != nil { + return nil, err + } + if _, err := p.expect(tokenRParen, "expected ')'"); err != nil { + return nil, err + } + return p.parsePostfix(expr) + case tokenLBrace: + return p.parseLambdaExpr() + case tokenBang, tokenMinus: + value, err := p.parseExpr(7) + if err != nil { + return nil, err + } + return UnaryExpr{Op: tok.lexeme, Value: value}, nil + default: + return nil, fmt.Errorf("unexpected token %q at %d", tok.lexeme, tok.pos) + } +} + +func (p *parser) parseNameList() ([]string, error) { + name, err := p.expect(tokenIdent, "expected variable name") + if err != nil { + return nil, err + } + names := []string{name.lexeme} + for p.match(tokenComma) { + next, err := p.expect(tokenIdent, "expected variable name") + if err != nil { + return nil, err + } + names = append(names, next.lexeme) + } + return names, nil +} + +func (p *parser) parseLambdaExpr() (Expr, error) { + params, implicitIt, err := p.parseLambdaParams() + if err != nil { + return nil, err + } + + var body []Stmt + for !p.check(tokenRBrace) && !p.check(tokenEOF) { + stmt, err := p.parseStmt() + if err != nil { + return nil, err + } + body = append(body, stmt) + p.match(tokenSemicolon) + } + if _, err := p.expect(tokenRBrace, "expected '}' after lambda body"); err != nil { + return nil, err + } + + return LambdaExpr{Params: params, ImplicitIt: implicitIt, Body: body}, nil +} + +func (p *parser) parseLambdaParams() ([]Param, bool, error) { + if p.match(tokenArrow) { + return nil, false, nil + } + + saved := p.pos + var params []Param + for p.check(tokenIdent) { + name := p.advance() + param := Param{Name: name.lexeme} + if p.match(tokenColon) { + typ, err := p.parseTypeRef() + if err != nil { + return nil, false, err + } + param.Type = typ + } + params = append(params, param) + if !p.match(tokenComma) { + break + } + } + + if len(params) == 0 { + return nil, true, nil + } + if p.match(tokenArrow) { + return params, false, nil + } + + if allUntypedLambdaParams(params) { + p.pos = saved + return nil, true, nil + } + return nil, false, fmt.Errorf("lambda parameters must either all specify types or all omit them") +} + +func (p *parser) parsePostfix(expr Expr) (Expr, error) { + for { + switch { + case p.match(tokenDot): + name, err := p.expect(tokenIdent, "expected selector name") + if err != nil { + return nil, err + } + expr = SelectorExpr{Receiver: expr, Name: name.lexeme} + case p.check(tokenLt): + typeArgs, hasTypeArgs, err := p.tryParseCallTypeArgs() + if err != nil { + return nil, err + } + if !hasTypeArgs { + return expr, nil + } + if _, err := p.expect(tokenLParen, "expected '(' after generic type arguments"); err != nil { + return nil, err + } + var args []Expr + if !p.check(tokenRParen) { + for { + arg, err := p.parseExpr(0) + if err != nil { + return nil, err + } + args = append(args, arg) + if !p.match(tokenComma) { + break + } + } + } + if _, err := p.expect(tokenRParen, "expected ')' after arguments"); err != nil { + return nil, err + } + expr = CallExpr{Callee: expr, Args: args, TypeArgs: typeArgs} + case p.match(tokenLParen): + var args []Expr + if !p.check(tokenRParen) { + for { + arg, err := p.parseExpr(0) + if err != nil { + return nil, err + } + args = append(args, arg) + if !p.match(tokenComma) { + break + } + } + } + if _, err := p.expect(tokenRParen, "expected ')' after arguments"); err != nil { + return nil, err + } + expr = CallExpr{Callee: expr, Args: args} + case p.check(tokenLBrace): + call, ok := expr.(CallExpr) + if !ok { + return expr, nil + } + lambda, err := p.parsePrefix() + if err != nil { + return nil, err + } + call.Args = append(call.Args, lambda) + expr = call + default: + return expr, nil + } + } +} + +func (p *parser) tryParseCallTypeArgs() ([]string, bool, error) { + if !p.check(tokenLt) { + return nil, false, nil + } + + saved := p.pos + p.advance() + + var typeArgs []string + for { + arg, err := p.parseTypeRef() + if err != nil { + p.pos = saved + return nil, false, nil + } + typeArgs = append(typeArgs, arg) + if !p.match(tokenComma) { + break + } + } + if !p.match(tokenGt) { + p.pos = saved + return nil, false, nil + } + if !p.check(tokenLParen) { + p.pos = saved + return nil, false, nil + } + return typeArgs, true, nil +} + +func (p *parser) parseTypeRef() (string, error) { + if p.match(tokenLParen) { + var params []string + if !p.check(tokenRParen) { + for { + paramType, err := p.parseTypeRef() + if err != nil { + return "", err + } + params = append(params, paramType) + if !p.match(tokenComma) { + break + } + } + } + if _, err := p.expect(tokenRParen, "expected ')' in function type"); err != nil { + return "", err + } + if _, err := p.expect(tokenArrow, "expected '->' in function type"); err != nil { + return "", err + } + ret, err := p.parseTypeRef() + if err != nil { + return "", err + } + return "(" + strings.Join(params, ", ") + ") -> " + ret, nil + } + + var b string + for p.match(tokenStar) { + b += "*" + } + + name, err := p.expect(tokenIdent, "expected type name") + if err != nil { + return "", err + } + b += name.lexeme + + for p.match(tokenDot) { + part, err := p.expect(tokenIdent, "expected selector name in type") + if err != nil { + return "", err + } + b += "." + part.lexeme + } + + if p.match(tokenLt) { + var args []string + for { + arg, err := p.parseTypeRef() + if err != nil { + return "", err + } + args = append(args, arg) + if !p.match(tokenComma) { + break + } + } + if _, err := p.expect(tokenGt, "expected '>' in generic type"); err != nil { + return "", err + } + b += "<" + strings.Join(args, ", ") + ">" + } + + return b, nil +} + +func (p *parser) parseImportPath() (string, error) { + if !importPathStarts(p.peek()) { + tok := p.peek() + return "", fmt.Errorf("expected import path at %d, found %q", tok.pos, tok.lexeme) + } + + var b strings.Builder + b.WriteString(p.advance().lexeme) + for { + if !(p.check(tokenDot) || p.check(tokenMinus)) { + break + } + sep := p.advance().lexeme + part, err := p.expect(tokenIdent, "expected import path segment") + if err != nil { + return "", err + } + b.WriteString(sep) + b.WriteString(part.lexeme) + } + return b.String(), nil +} + +func importPathStarts(tok token) bool { + return tok.kind == tokenIdent +} + +func importPathContinues(tok token) bool { + switch tok.kind { + case tokenIdent: + return true + default: + return false + } +} + +func validateBareImportPath(path string) error { + if strings.HasPrefix(path, `"`) { + return nil + } + return nil +} + +func allUntypedLambdaParams(params []Param) bool { + if len(params) == 0 { + return false + } + for _, param := range params { + if param.Type != "" { + return false + } + } + return true +} + +func infixPrecedence(kind tokenKind) (int, bool) { + switch kind { + case tokenOr: + return 1, true + case tokenAnd: + return 2, true + case tokenEq, tokenNeq: + return 3, true + case tokenLt, tokenLte, tokenGt, tokenGte: + return 4, true + case tokenPlus, tokenMinus: + return 5, true + case tokenStar, tokenSlash, tokenPercent: + return 6, true + default: + return 0, false + } +} + +func (p *parser) expect(kind tokenKind, msg string) (token, error) { + if !p.check(kind) { + tok := p.peek() + return token{}, fmt.Errorf("%s at %d, found %q", msg, tok.pos, tok.lexeme) + } + return p.advance(), nil +} + +func (p *parser) match(kind tokenKind) bool { + if p.check(kind) { + p.advance() + return true + } + return false +} + +func (p *parser) check(kind tokenKind) bool { + return p.peek().kind == kind +} + +func (p *parser) advance() token { + tok := p.tokens[p.pos] + if p.pos < len(p.tokens)-1 { + p.pos++ + } + return tok +} + +func (p *parser) peek() token { + return p.tokens[p.pos] +} + +func (p *parser) peekN(offset int) token { + idx := p.pos + offset + if idx >= len(p.tokens) { + return p.tokens[len(p.tokens)-1] + } + return p.tokens[idx] +} diff --git a/internal/lang/token.go b/internal/lang/token.go new file mode 100644 index 0000000..f63a240 --- /dev/null +++ b/internal/lang/token.go @@ -0,0 +1,86 @@ +package lang + +type tokenKind string + +const ( + tokenEOF tokenKind = "EOF" + tokenIdent tokenKind = "IDENT" + tokenInt tokenKind = "INT" + tokenString tokenKind = "STRING" + tokenTrue tokenKind = "TRUE" + tokenFalse tokenKind = "FALSE" + tokenNull tokenKind = "NULL" + tokenImport tokenKind = "IMPORT" + tokenPackage tokenKind = "PACKAGE" + tokenClass tokenKind = "CLASS" + tokenWorker tokenKind = "WORKER" + tokenInterface tokenKind = "INTERFACE" + tokenFun tokenKind = "FUN" + tokenOverride tokenKind = "OVERRIDE" + tokenVal tokenKind = "VAL" + tokenVar tokenKind = "VAR" + tokenIf tokenKind = "IF" + tokenElse tokenKind = "ELSE" + tokenWhile tokenKind = "WHILE" + tokenSelect tokenKind = "SELECT" + tokenReturn tokenKind = "RETURN" + tokenGo tokenKind = "GO" + tokenTry tokenKind = "TRY" + tokenCatch tokenKind = "CATCH" + tokenThrow tokenKind = "THROW" + tokenLParen tokenKind = "(" + tokenRParen tokenKind = ")" + tokenLBrace tokenKind = "{" + tokenRBrace tokenKind = "}" + tokenComma tokenKind = "," + tokenDot tokenKind = "." + tokenColon tokenKind = ":" + tokenSemicolon tokenKind = ";" + tokenPlus tokenKind = "+" + tokenMinus tokenKind = "-" + tokenStar tokenKind = "*" + tokenSlash tokenKind = "/" + tokenPercent tokenKind = "%" + tokenBang tokenKind = "!" + tokenAssign tokenKind = "=" + tokenPlusAssign tokenKind = "+=" + tokenEq tokenKind = "==" + tokenNeq tokenKind = "!=" + tokenLt tokenKind = "<" + tokenLte tokenKind = "<=" + tokenGt tokenKind = ">" + tokenGte tokenKind = ">=" + tokenAnd tokenKind = "&&" + tokenOr tokenKind = "||" + tokenArrow tokenKind = "->" +) + +var keywords = map[string]tokenKind{ + "fun": tokenFun, + "import": tokenImport, + "package": tokenPackage, + "class": tokenClass, + "worker": tokenWorker, + "interface": tokenInterface, + "val": tokenVal, + "var": tokenVar, + "override": tokenOverride, + "if": tokenIf, + "else": tokenElse, + "while": tokenWhile, + "select": tokenSelect, + "return": tokenReturn, + "go": tokenGo, + "try": tokenTry, + "catch": tokenCatch, + "throw": tokenThrow, + "true": tokenTrue, + "false": tokenFalse, + "null": tokenNull, +} + +type token struct { + kind tokenKind + lexeme string + pos int +} diff --git a/tools/vscode-gotlin/README.md b/tools/vscode-gotlin/README.md new file mode 100644 index 0000000..7a27b92 --- /dev/null +++ b/tools/vscode-gotlin/README.md @@ -0,0 +1,58 @@ +# vscode-gotlin + +Minimal VS Code extension for `.gt` files. + +It does two things: + +- registers `.gt` as the `gotlin` language +- launches `gotlin-lsp` over stdio + +It also includes: + +- syntax highlighting +- bracket/comment configuration +- basic Gotlin snippets +- optional `gopls` bridge for hover/definition on Go-imported symbols + +## Setup + +From this folder: + +```bash +npm install +npm run build +``` + +Then in VS Code: + +1. Open this folder as an extension project. +2. Press `F5` to launch an Extension Development Host. +3. Open your Gotlin workspace in that host. + +## Server path + +By default the extension looks for: + +```text +/bin/gotlin-lsp +``` + +If your binary lives somewhere else, set: + +```json +"gotlin.serverPath": "/absolute/path/to/gotlin-lsp" +``` + +If `gopls` is not on your PATH, also set: + +```json +"gotlin.goplsPath": "/absolute/path/to/gopls" +``` + +## Build the language server + +From the repo root: + +```bash +go build -o ./bin/gotlin-lsp ./cmd/gotlin-lsp +``` diff --git a/tools/vscode-gotlin/language-configuration.json b/tools/vscode-gotlin/language-configuration.json new file mode 100644 index 0000000..867069a --- /dev/null +++ b/tools/vscode-gotlin/language-configuration.json @@ -0,0 +1,56 @@ +{ + "comments": { + "lineComment": "//" + }, + "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\"\\'\\,\\.\\<\\>\\/\\?\\s]+)", + "brackets": [ + [ + "{", + "}" + ], + [ + "(", + ")" + ] + ], + "autoClosingPairs": [ + { + "open": "{", + "close": "}" + }, + { + "open": "(", + "close": ")" + }, + { + "open": "\"", + "close": "\"" + } + ], + "surroundingPairs": [ + [ + "{", + "}" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ] + ], + "indentationRules": { + "increaseIndentPattern": "^.*\\{\\s*$", + "decreaseIndentPattern": "^\\s*\\}" + }, + "onEnterRules": [ + { + "beforeText": "^.*\\{\\s*$", + "action": { + "indent": "indent" + } + } + ] +} diff --git a/tools/vscode-gotlin/package-lock.json b/tools/vscode-gotlin/package-lock.json new file mode 100644 index 0000000..6c0686d --- /dev/null +++ b/tools/vscode-gotlin/package-lock.json @@ -0,0 +1,140 @@ +{ + "name": "gotlin-vscode", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gotlin-vscode", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/vscode": "^1.90.0", + "typescript": "^5.5.4" + }, + "engines": { + "vscode": "^1.90.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.109.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", + "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + } + } +} diff --git a/tools/vscode-gotlin/package.json b/tools/vscode-gotlin/package.json new file mode 100644 index 0000000..17b5a2e --- /dev/null +++ b/tools/vscode-gotlin/package.json @@ -0,0 +1,73 @@ +{ + "name": "gotlin-vscode", + "displayName": "Gotlin", + "description": "VS Code support for Gotlin (.gt) files", + "version": "0.0.1", + "publisher": "local", + "license": "UNLICENSED", + "engines": { + "vscode": "^1.90.0" + }, + "categories": [ + "Programming Languages" + ], + "activationEvents": [ + + ], + "main": "./out/extension.js", + "contributes": { + "languages": [ + { + "id": "gotlin", + "aliases": [ + "Gotlin", + "gotlin" + ], + "extensions": [ + ".gt" + ], + "configuration": "./language-configuration.json" + } + ], + "snippets": [ + { + "language": "gotlin", + "path": "./snippets/gotlin.code-snippets" + } + ], + "grammars": [ + { + "language": "gotlin", + "scopeName": "source.gotlin", + "path": "./syntaxes/gotlin.tmLanguage.json" + } + ], + "configuration": { + "title": "Gotlin", + "properties": { + "gotlin.serverPath": { + "type": "string", + "default": "", + "description": "Absolute path to the gotlin-lsp binary. If empty, the extension tries /bin/gotlin-lsp first, then 'gotlin-lsp' on PATH." + }, + "gotlin.goplsPath": { + "type": "string", + "default": "", + "description": "Absolute path to gopls. If empty, gotlin-lsp tries 'gopls' on PATH." + } + } + } + }, + "scripts": { + "build": "tsc -p .", + "watch": "tsc -w -p ." + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/vscode": "^1.90.0", + "typescript": "^5.5.4" + } +} diff --git a/tools/vscode-gotlin/snippets/gotlin.code-snippets b/tools/vscode-gotlin/snippets/gotlin.code-snippets new file mode 100644 index 0000000..febc051 --- /dev/null +++ b/tools/vscode-gotlin/snippets/gotlin.code-snippets @@ -0,0 +1,59 @@ +{ + "Function": { + "prefix": "fun", + "body": [ + "fun ${1:name}(${2}): ${3:Unit} {", + " $0", + "}" + ], + "description": "Gotlin function" + }, + "Main Function": { + "prefix": "main", + "body": [ + "fun main() {", + " $0", + "}" + ], + "description": "Gotlin main function" + }, + "Package": { + "prefix": "package", + "body": [ + "package ${1:demo}" + ], + "description": "Gotlin package declaration" + }, + "Go Import": { + "prefix": "importgo", + "body": [ + "import go.${1:fmt}" + ], + "description": "Import a Go package" + }, + "If": { + "prefix": "if", + "body": [ + "if (${1:condition}) {", + " $0", + "}" + ], + "description": "If statement" + }, + "Lambda": { + "prefix": "lambda", + "body": [ + "{ ${1:it} -> $0 }" + ], + "description": "Lambda expression" + }, + "Override Method": { + "prefix": "override", + "body": [ + "override fun ${1:name}(${2}): ${3:Unit} {", + " $0", + "}" + ], + "description": "Override a class or interface method" + } +} diff --git a/tools/vscode-gotlin/src/extension.ts b/tools/vscode-gotlin/src/extension.ts new file mode 100644 index 0000000..613456a --- /dev/null +++ b/tools/vscode-gotlin/src/extension.ts @@ -0,0 +1,86 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as vscode from "vscode"; +import { + LanguageClient, + LanguageClientOptions, + ServerOptions +} from "vscode-languageclient/node"; + +let client: LanguageClient | undefined; + +export async function activate(context: vscode.ExtensionContext): Promise { + const serverPath = resolveServerPath(); + if (!serverPath) { + void vscode.window.showErrorMessage( + "Gotlin LSP binary was not found. Build ./bin/gotlin-lsp or set gotlin.serverPath." + ); + return; + } + + const serverOptions: ServerOptions = { + command: serverPath, + args: [], + options: { + env: { + ...process.env, + GOTLIN_GOPLS_PATH: resolveGoplsPath() + } + } + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: "file", language: "gotlin" }], + outputChannelName: "Gotlin Language Server" + }; + + client = new LanguageClient( + "gotlin-lsp", + "Gotlin Language Server", + serverOptions, + clientOptions + ); + + await client.start(); +} + +export async function deactivate(): Promise { + if (client) { + await client.stop(); + client = undefined; + } +} + +function resolveServerPath(): string | undefined { + const configured = vscode.workspace + .getConfiguration("gotlin") + .get("serverPath", "") + .trim(); + + if (configured) { + return configured; + } + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (workspaceFolder) { + const candidate = path.join(workspaceFolder.uri.fsPath, "bin", platformBinaryName("gotlin-lsp")); + if (fs.existsSync(candidate)) { + return candidate; + } + } + + return "gotlin-lsp"; +} + +function platformBinaryName(base: string): string { + return process.platform === "win32" ? `${base}.exe` : base; +} + +function resolveGoplsPath(): string { + const configured = vscode.workspace + .getConfiguration("gotlin") + .get("goplsPath", "") + .trim(); + + return configured; +} diff --git a/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json b/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json new file mode 100644 index 0000000..9881661 --- /dev/null +++ b/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json @@ -0,0 +1,203 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Gotlin", + "scopeName": "source.gotlin", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#imports" + }, + { + "include": "#package" + }, + { + "include": "#functions" + }, + { + "include": "#typesDecl" + }, + { + "include": "#literals" + }, + { + "include": "#keywords" + }, + { + "include": "#types" + }, + { + "include": "#strings" + }, + { + "include": "#numbers" + }, + { + "include": "#operators" + } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.double-slash.gotlin", + "match": "//.*$" + } + ] + }, + "imports": { + "patterns": [ + { + "name": "meta.import.gotlin", + "match": "\\b(import)\\b\\s+((?:[A-Za-z_][\\w-]*\\.)*[A-Za-z_][\\w-]*)(?:\\s+((?:[A-Za-z_][\\w-]*\\.)*[A-Za-z_][\\w-]*))?", + "captures": { + "1": { + "name": "keyword.control.import.gotlin" + }, + "2": { + "name": "meta.path.gotlin" + }, + "3": { + "name": "meta.path.gotlin" + } + } + } + ] + }, + "package": { + "patterns": [ + { + "name": "meta.package.gotlin", + "match": "\\b(package)\\b\\s+((?:[A-Za-z_][\\w-]*\\.)*[A-Za-z_][\\w-]*)", + "captures": { + "1": { + "name": "keyword.control.package.gotlin" + }, + "2": { + "name": "meta.path.gotlin" + } + } + } + ] + }, + "functions": { + "patterns": [ + { + "name": "meta.function.gotlin", + "match": "\\b(?:(override)\\s+)?(fun)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)", + "captures": { + "1": { + "name": "storage.modifier.gotlin" + }, + "2": { + "name": "keyword.control.function.gotlin" + }, + "3": { + "name": "entity.name.function.gotlin" + } + } + } + ] + }, + "typesDecl": { + "patterns": [ + { + "name": "meta.interface.gotlin", + "match": "\\b(interface)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)", + "captures": { + "1": { + "name": "storage.type.interface.gotlin" + }, + "2": { + "name": "entity.name.type.interface.gotlin" + } + } + }, + { + "name": "meta.class.gotlin", + "match": "\\b(class)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)", + "captures": { + "1": { + "name": "storage.type.class.gotlin" + }, + "2": { + "name": "entity.name.type.class.gotlin" + } + } + } + ] + }, + "literals": { + "patterns": [ + { + "name": "constant.language.boolean.gotlin", + "match": "\\b(true|false)\\b" + }, + { + "name": "constant.language.null.gotlin", + "match": "\\bnull\\b" + }, + { + "name": "variable.language.this.gotlin", + "match": "\\bthis\\b" + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.control.gotlin", + "match": "\\b(fun|val|var|override|if|else|while|return|try|catch|throw)\\b" + } + ] + }, + "types": { + "patterns": [ + { + "name": "storage.type.gotlin", + "match": "\\b(Int|String|Boolean|Unit)\\b" + }, + { + "name": "support.type.gotlin", + "match": "\\b[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)+\\b" + }, + { + "name": "entity.name.type.gotlin", + "match": "\\b[A-Z][A-Za-z0-9_]*\\b" + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.gotlin", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "name": "constant.character.escape.gotlin", + "match": "\\\\." + } + ] + } + ] + }, + "numbers": { + "patterns": [ + { + "name": "constant.numeric.gotlin", + "match": "\\b\\d+\\b" + } + ] + }, + "operators": { + "patterns": [ + { + "name": "keyword.operator.gotlin", + "match": "->|==|!=|<=|>=|&&|\\|\\||[=+\\-*/%<>!:.,]" + } + ] + } + } +} diff --git a/tools/vscode-gotlin/tsconfig.json b/tools/vscode-gotlin/tsconfig.json new file mode 100644 index 0000000..b6609e0 --- /dev/null +++ b/tools/vscode-gotlin/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": [ + "ES2020" + ], + "outDir": "out", + "rootDir": "src", + "strict": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +}