diff --git a/README.md b/README.md index 74f83ae..dfd6b29 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This is the practical boundary of the prototype: - `fun` declarations - `val` and `var` -- `Int`, `String`, `Boolean`, `Unit` +- `Int`, `Long`, `String`, `Boolean`, `Unit` - function types like `(String) -> Unit` - `if`, `else`, `while`, and `for (item in items)` - function calls @@ -23,8 +23,11 @@ This is the practical boundary of the prototype: - `println(...)` - arithmetic, comparison, and boolean operators - decimal literals, `defer`, and Go address-of expressions such as `&value` +- explicit nullable types (`Type?`), safe access (`?.`), and non-null assertions (`!!`) +- Gotlin classes are reference types by default; `*` is only needed for external Go pointer types - named external Go struct construction, for example `http.Client(timeout = 3 * time.second)` - top-level embedded resources such as `@embed("assets/*") val assets: embed.FS` +- structured coroutines with `suspend fun`, `runBlocking`, `coroutineScope`, `launch`, `async`, `await`, `delay`, `withTimeout`, and `isActive` ## Example @@ -118,6 +121,58 @@ fun describe(result: PaymentResult): String { Enum matches must contain each variant exactly once. Variant payload arity is checked during Gotlin compilation. +## Null safety + +Types are non-nullable by default. Add `?` explicitly when `null` is valid: + +```kotlin +fun email(user: User?): String? { + return user?.email +} +``` + +The compiler rejects `null` in non-nullable arguments, fields, local variables, +and return values. Nullable receivers cannot be dereferenced directly. Gotlin +smart-casts values after `value != null` branches and guard clauses such as +`if (value == null) { return }`. Use `!!` only when an invariant cannot be +expressed through control flow: + +```kotlin +val required: User = optionalUser!! +``` + +## Error handling + +Gotlin adapts Go `(T, error)` returns into Rust-style `Result` flows. +An explicit `Result` return or variable type converts the Go return directly: + +```kotlin +fun parse(value: String): Result { + return strconv.atoi(value) +} + +fun ping(db: *sql.DB): Result { + return db.ping() +} +``` + +Use `?` to propagate a Go error or chain a Gotlin function returning `Result`. +Explicit panic and fallback operations are available when appropriate: + +```kotlin +val required = parse("42").unwrap() +val fallback = parse("invalid").unwrapOr(0) +``` + +Gotlin never inserts implicit panic wrappers. A Go call returning `(T, error)` +must have an explicit `Result` context or use `?`, explicit `value, error` +destructuring, or `.unwrap()`. + +Gotlin-defined `class` and `data class` values are references automatically, +including nested generic types such as `List`. Explicit pointer syntax is +still supported for compatibility. Go interop remains explicit, for example +`*http.Request` and `*pgxpool.Pool`. + Enums whose variants carry no payload are represented as string-backed values. The exact variant identifier is used for JSON and PostgreSQL text values: @@ -148,11 +203,11 @@ When the surrounding expression provides a target type, the type argument is optional: ```kotlin -fun response(account: *AccountEntity): *AccountResponse { +fun response(account: AccountEntity): AccountResponse { return account.mapTo() } -val response: *AccountResponse = account.mapTo() +val response: AccountResponse = account.mapTo() val envelope = Envelope(account.mapTo()) ``` @@ -192,6 +247,28 @@ Run directly: go run ./cmd/gotlinc run ./examples/hello.gt ``` +## Structured coroutines + +Gotlin coroutines use Go goroutines underneath, but expose only structured +scopes. A scope waits for its children, propagates child failures, and cancels +sibling coroutine contexts. The removed `worker` and bare `go` forms are not +valid Gotlin syntax. + +```kotlin +suspend fun load(): Int { + delay(10) + return 42 +} + +fun main() { + runBlocking { + val value = async { return load() } + launch { println("loading") } + println(value.await()) + } +} +``` + ## Type-checked SQL queries Gotlin recognizes a PostgreSQL SQL DSL at compile time. SQL row mappings must be data classes annotated with `@table`. Fields map from lower-camel Gotlin names to `snake_case` columns by default and can override the SQL name with `@column`. Conflict keys use `@id`; database-generated or defaulted fields use `@generated`. @@ -262,17 +339,20 @@ Projection targets are local data classes. The constructor must contain one dire ```kotlin data class AccountSummary(var id: String, var balance: Double) -fun summaries(pool: *pgxpool.Pool, ctx: context.Context): List<*AccountSummary> { +fun summaries(pool: *pgxpool.Pool, ctx: context.Context): List { return sql.from() .select { row -> AccountSummary(row.id, row.balance) } .orderBy { it.balance } .fetch(pool, ctx) + .unwrap() } ``` The compiler emits `SELECT id, balance`, scans in projection declaration order, and makes `fetch`, `single`, and `iterator` target `AccountSummary` rather than `AccountRow`. The row parameter in later `where` and ordering methods still represents `AccountRow`. -Select chains can execute directly against a pgx/v5 pool. `fetch(pool, ctx)` returns `List<*AccountRow>` and closes the pgx rows after reading and scanning every result in data-class field declaration order: +Select chains can execute directly against a pgx/v5 pool. `fetch(pool, ctx)` +returns `Result, Error>` and closes pgx rows after reading and +scanning every result in data-class field declaration order: ```kotlin import context @@ -282,33 +362,38 @@ fun accountsFor( pool: *pgxpool.Pool, ctx: context.Context, customerId: String -): List<*AccountRow> { +): List { return sql.from() .where { it.customerId == customerId } .orderBy { it.accountType } .fetch(pool, ctx) + .unwrap() } ``` -`single(pool, ctx)` returns `*AccountRow`. It closes the rows and panics through Gotlin's auto-throw path unless the query produces exactly one row: +`single(pool, ctx)` returns `Result` and closes the rows. Use +`?` to propagate zero/multiple-row errors or call `.unwrap()` explicitly: ```kotlin -fun account(pool: *pgxpool.Pool, ctx: context.Context, id: String): *AccountRow { +fun account(pool: *pgxpool.Pool, ctx: context.Context, id: String): AccountRow { return sql.from() .where { it.id == id } .single(pool, ctx) + .unwrap() } ``` -`iterator(pool, ctx)` streams pgx rows. Call `next()` before each `value()`, always arrange an explicit `close()` (normally with `defer`), and call `err()` after iteration. Assigning the error result invokes the existing auto-throw convention: +`iterator(pool, ctx)` returns a `Result` around a streaming pgx iterator. Call +`next()` before each `value()`, arrange an explicit `close()`, and inspect +`err()` after iteration: ```kotlin fun printAccounts(pool: *pgxpool.Pool, ctx: context.Context) { - val rows = sql.from().iterator(pool, ctx) + val rows = sql.from().iterator(pool, ctx).unwrap() defer rows.close() while (rows.next()) { - val account: *AccountRow = rows.value() + val account: AccountRow = rows.value() println(account.customerId) } @@ -316,7 +401,11 @@ fun printAccounts(pool: *pgxpool.Pool, ctx: context.Context) { } ``` -Query and scan failures panic through `gotlinAutoThrow`. `fetch` and `single` close rows internally; an iterator leaves lifecycle control with the caller, and `value()` panics unless the immediately preceding `next()` succeeded. None of the execution terminals require an intermediate `build()`, `query.sql`, or `query.args` access. `build()` remains available when a query value is needed for manual execution. +Query and scan failures are returned as `Result` errors. `fetch` and `single` +close rows internally; an iterator leaves lifecycle control with the caller, +and `value()` panics only when called without a successful `next()`. None of the +execution terminals require an intermediate `build()`, `query.sql`, or +`query.args` access. `build()` remains available for manual execution. ### Inserts and returning diff --git a/cmd/gotlin-lsp/main.go b/cmd/gotlin-lsp/main.go index e520cb6..0e8d59b 100644 --- a/cmd/gotlin-lsp/main.go +++ b/cmd/gotlin-lsp/main.go @@ -998,6 +998,10 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma } case lang.SelectorExpr: walkExpr(e.Receiver, scope) + case lang.SafeSelectorExpr: + walkExpr(e.Receiver, scope) + case lang.NonNullExpr: + walkExpr(e.Value, scope) case lang.IndexExpr: walkExpr(e.Receiver, scope) walkExpr(e.Index, scope) @@ -2192,33 +2196,42 @@ func builtinHoverDetail(name string) (string, bool) { } var builtinDetails = map[string]string{ - "println": "fun println(value: Any): Unit", - "runCatching": "fun runCatching(block: () -> Unit): Result", - "Channel": "fun Channel(capacity: Int = 0): Channel", - "after": "fun after(ms: Int): Channel", - "every": "fun every(ms: Int): Channel", - "listOf": "fun listOf(values: T...): List", - "mutableListOf": "fun mutableListOf(values: T...): MutableList", - "mapOf": "fun mapOf(pairs: Any...): Map", - "mutableMapOf": "fun mutableMapOf(pairs: Any...): MutableMap", - "ByteSlice": "fun ByteSlice(value: String): ByteSlice", - "append": "fun append(values: List, value: T): List", - "len": "fun len(value: Any): Int", - "cap": "fun cap(value: Any): Int", - "make": "fun make(size: Int): T", - "new": "fun new(): *T", - "copy": "fun copy(target: Any, source: Any): Int", - "delete": "fun delete(map: Any, key: Any): Unit", - "close": "fun close(channel: Any): Unit", - "panic": "fun panic(value: Any): Unit", - "recover": "fun recover(): Any", - "string": "fun string(value: Any): String", - "int": "fun int(value: Any): Int", - "float64": "fun float64(value: Any): Double", - "bool": "fun bool(value: Any): Boolean", - "sql": "typed PostgreSQL query DSL", - "set": "fun set(target: Any, value: Any): Unit", - "now": "fun now(): time.Time", + "println": "fun println(value: Any): Unit", + "runCatching": "fun runCatching(block: () -> Unit): Result", + "Channel": "fun Channel(capacity: Int = 0): Channel", + "after": "fun after(ms: Int): Channel", + "every": "fun every(ms: Int): Channel", + "listOf": "fun listOf(values: T...): List", + "mutableListOf": "fun mutableListOf(values: T...): MutableList", + "mapOf": "fun mapOf(pairs: Any...): Map", + "mutableMapOf": "fun mutableMapOf(pairs: Any...): MutableMap", + "ByteSlice": "fun ByteSlice(value: String | Int): ByteSlice", + "append": "fun append(values: List, value: T): List", + "keys": "fun keys(values: Map): List", + "goAssert": "fun goAssert(value: Any): T", + "len": "fun len(value: Any): Int", + "cap": "fun cap(value: Any): Int", + "make": "fun make(size: Int): T", + "new": "fun new(): *T", + "copy": "fun copy(target: Any, source: Any): Int", + "delete": "fun delete(map: Any, key: Any): Unit", + "close": "fun close(channel: Any): Unit", + "panic": "fun panic(value: Any): Unit", + "recover": "fun recover(): Any", + "string": "fun string(value: Any): String", + "int": "fun int(value: Any): Int", + "float64": "fun float64(value: Any): Double", + "bool": "fun bool(value: Any): Boolean", + "sql": "typed PostgreSQL query DSL", + "set": "fun set(target: Any, value: Any): Unit", + "now": "fun now(): time.Time", + "runBlocking": "fun runBlocking(block: suspend () -> Unit): Unit", + "coroutineScope": "suspend fun coroutineScope(block: suspend () -> Unit): Unit", + "launch": "suspend fun launch(block: suspend () -> Unit): Unit", + "async": "suspend fun async(block: suspend () -> T): Deferred", + "delay": "suspend fun delay(ms: Int): Unit", + "withTimeout": "suspend fun withTimeout(ms: Int, block: suspend () -> Unit): Unit", + "isActive": "suspend fun isActive(): Boolean", } func contains(values []string, needle string) bool { diff --git a/cmd/gotlin-lsp/main_test.go b/cmd/gotlin-lsp/main_test.go index 5ba652a..4128e6a 100644 --- a/cmd/gotlin-lsp/main_test.go +++ b/cmd/gotlin-lsp/main_test.go @@ -92,7 +92,7 @@ fun main() { t.Fatalf("unexpected builtin diagnostic: %+v", diagnostic) } } - for _, name := range []string{"ByteSlice", "append", "len", "sql", "set", "now"} { + for _, name := range []string{"ByteSlice", "append", "keys", "goAssert", "len", "sql", "set", "now"} { if !isBuiltin(name) { t.Fatalf("%s is not registered as builtin", name) } @@ -694,7 +694,7 @@ fun runWorker(name: String) { } fun main() { - go runWorker("alice") + runBlocking { launch { runWorker("alice") } } } `) @@ -717,9 +717,11 @@ fun writer(ch: Channel) { fun main() { val ch = Channel() - go writer(ch) - select { - ch -> println(it) + runBlocking { + launch { writer(ch) } + select { + ch -> println(it) + } } val v = ch.read() println(v) @@ -778,22 +780,11 @@ fun main() { `) state := buildDocumentState(text) - if state.program == nil { - t.Fatal("expected parsed program") + if state.program != nil { + t.Fatal("worker syntax should no longer parse") } - 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) + if len(state.diagnostics) == 0 { + t.Fatal("expected removed worker syntax diagnostic") } } diff --git a/examples/http_server.gt b/examples/http_server.gt index a528fe4..c7a11bf 100644 --- a/examples/http_server.gt +++ b/examples/http_server.gt @@ -36,18 +36,6 @@ class EpicControllerImpl(val db: *bun.DB) { } } -worker Counter { - var 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( @@ -56,21 +44,10 @@ fun main() { ) ) 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/mapping.gt b/examples/mapping.gt index 8956e1a..3b866d6 100644 --- a/examples/mapping.gt +++ b/examples/mapping.gt @@ -16,13 +16,13 @@ enum ResponseState { data class AccountEntity( var id: String, - var address: *AddressEntity, + var address: AddressEntity, var labels: List, var state: EntityState ) data class AccountResponse( - var address: *AddressResponse, + var address: AddressResponse, var id: String, var labels: List, var state: ResponseState diff --git a/examples/nullability.gt b/examples/nullability.gt new file mode 100644 index 0000000..a7d236e --- /dev/null +++ b/examples/nullability.gt @@ -0,0 +1,18 @@ +package main + +data class User(var email: String) + +fun emailOrMissing(user: User?): String { + if (user == null) { return "missing" } + return user.email +} + +fun optionalEmail(user: User?): String? { + return user?.email +} + +fun main() { + val user: User? = User("user@example.com") + println(emailOrMissing(user)) + println(user!!.email) +} diff --git a/examples/results.gt b/examples/results.gt new file mode 100644 index 0000000..79d376a --- /dev/null +++ b/examples/results.gt @@ -0,0 +1,18 @@ +package main + +import strconv +import os + +fun parse(value: String): Result { + return strconv.atoi(value) +} + +fun changeDirectory(path: String): Result { + return os.chdir(path) +} + +fun main() { + println(parse("42").unwrap()) + println(parse("invalid").unwrapOr(0)) + changeDirectory(".").unwrap() +} diff --git a/examples/showcase.gt b/examples/showcase.gt index 6f281ed..4d2ec3d 100644 --- a/examples/showcase.gt +++ b/examples/showcase.gt @@ -13,25 +13,6 @@ class PrefixGreeter(val prefix: String): Greeter { } } -worker Counter { - var 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")) @@ -44,35 +25,25 @@ fun main() { 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 + val maybe: String? = 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") + runBlocking { + val ready = Channel() + launch { + delay(120) + ready.send("timer fired") + } + select { + ready -> println("channel says: " + it) } - } - select { - ready -> println("channel says: " + it) - } - select { - every(50) -> println("one periodic tick") + val answer = async { + delay(50) + return 42 + } + fmt.Println("async value:", answer.await()) } } diff --git a/internal/lang/ast.go b/internal/lang/ast.go index 04a7300..a0dfa9e 100644 --- a/internal/lang/ast.go +++ b/internal/lang/ast.go @@ -71,6 +71,7 @@ type FunctionSignature struct { Name string Params []Param ReturnType string + Suspend bool } type FunctionDecl struct { @@ -78,6 +79,7 @@ type FunctionDecl struct { Params []Param ReturnType string Body []Stmt + Suspend bool } type Param struct { @@ -290,6 +292,21 @@ type SelectorExpr struct { func (SelectorExpr) exprNode() {} +type SafeSelectorExpr struct { + Receiver Expr + Name string +} + +func (SafeSelectorExpr) exprNode() {} + +type NonNullExpr struct{ Value Expr } + +func (NonNullExpr) exprNode() {} + +type TryExpr struct{ Value Expr } + +func (TryExpr) exprNode() {} + type IndexExpr struct { Receiver Expr Index Expr diff --git a/internal/lang/compiler_test.go b/internal/lang/compiler_test.go index 4ee5917..2925c2f 100644 --- a/internal/lang/compiler_test.go +++ b/internal/lang/compiler_test.go @@ -70,7 +70,7 @@ fun main() { for _, want := range []string{ `"strings"`, `rand "math/rand"`, - `upper := gotlinAutoThrow(strings.ToUpper("go"))`, + `upper := strings.ToUpper("go")`, `fmt.Println(rand.Intn(3))`, } { if !strings.Contains(code, want) { @@ -294,7 +294,7 @@ fun main() { `func (self *Greeter) greet() {`, `fmt.Println("hello, " + self.name)`, `fmt.Println(self.name)`, - `greeter := gotlinAutoThrow(NewGreeter("world"))`, + `greeter := NewGreeter("world")`, } { if !strings.Contains(code, want) { t.Fatalf("generated Go missing %q:\n%s", want, code) @@ -388,7 +388,7 @@ fun main() { `type EpicControllerImpl struct {`, `func NewEpicControllerImpl() *EpicControllerImpl`, `func (self *EpicControllerImpl) hello(w http.ResponseWriter, r *http.Request) {`, - `var epicController EpicController = gotlinAutoThrow(NewEpicControllerImpl())`, + `var epicController EpicController = NewEpicControllerImpl()`, `http.HandleFunc("/", epicController.hello)`, `http.ListenAndServe(":8080", http.DefaultServeMux)`, } { @@ -546,7 +546,7 @@ fun main() { `type gotlinResult struct {`, `func gotlinRunCatching(fn func()) (result gotlinResult) {`, `panic("boom")`, - `result := gotlinAutoThrow(gotlinRunCatching(func() {`, + `result := gotlinRunCatching(func() {`, `if recovered := recover(); recovered != nil {`, `e := recovered`, } { @@ -556,7 +556,7 @@ fun main() { } } -func TestGenerateGoAutoThrowForGoErrorResults(t *testing.T) { +func TestGenerateGoExplicitErrorsForGoErrorResults(t *testing.T) { src := ` package demo @@ -569,6 +569,7 @@ fun main() { throw err } } + ` prog, err := Parse(src) @@ -583,9 +584,8 @@ fun main() { 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())`, + `db := sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable")`, + `err := db.Ping()`, `if err != nil {`, `panic(err)`, } { @@ -595,7 +595,32 @@ fun main() { } } -func TestGenerateGoAutoThrowForErrorExprStmt(t *testing.T) { +func TestGenerateGoMigrationInteropHelpers(t *testing.T) { + prog, err := Parse(`package demo +data class Versioned(var version: Long) +fun names(values: Map): List { return keys(values) } +fun buffer(): ByteSlice { return ByteSlice(32) } +fun optionalError(): Error? { return null }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "Version int64", + "for key := range values", + "return make([]byte, 32)", + "func optionalError() error", + } { + if !strings.Contains(string(out), want) { + t.Fatalf("generated Go missing %q:\n%s", want, out) + } + } +} + +func TestGenerateGoExplicitErrorsForErrorExprStmt(t *testing.T) { src := ` package demo @@ -619,7 +644,7 @@ fun main() { code := string(out) for _, want := range []string{ - `db := gotlinAutoThrow(sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable"))`, + `db := sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable")`, `db.Ping()`, } { if !strings.Contains(code, want) { @@ -654,7 +679,7 @@ fun main() { for _, want := range []string{ `type User struct {`, `func NewUser(Name string) *User`, - `user := gotlinAutoThrow(NewUser("alice"))`, + `user := NewUser("alice")`, `fmt.Println(user.Name)`, } { if !strings.Contains(code, want) { @@ -723,8 +748,8 @@ fun main() { 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})`, + `var names []string = []string{"alice", "bob"}`, + `var ages map[string]int = map[string]int{"alice": 30, "bob": 25}`, `fmt.Println(names)`, `fmt.Println(ages)`, } { @@ -758,8 +783,8 @@ fun main() { code := string(out) for _, want := range []string{ - `test := gotlinAutoThrow([]int{})`, - `labels := gotlinAutoThrow(map[string]int{})`, + `test := []int{}`, + `labels := map[string]int{}`, `fmt.Println(test)`, `fmt.Println(labels)`, } { @@ -791,7 +816,7 @@ fun main() { code := string(out) for _, want := range []string{ - `_ = gotlinAutoThrow([]int{1, 7})`, + `_ = []int{1, 7}`, `fmt.Println("ok")`, } { if !strings.Contains(code, want) { @@ -814,7 +839,7 @@ fun runWorker(name: String) { } fun main() { - go runWorker("alice") + runBlocking { launch { runWorker("alice") } } } ` @@ -832,9 +857,9 @@ fun main() { for _, want := range []string{ `func runWorker(name string)`, `fmt.Println(name)`, - `go func() {`, + `gotlinScope.Launch`, `runWorker("alice")`, - `println("async panic:", recovered)`, + `gotlinRunBlocking`, } { if !strings.Contains(code, want) { t.Fatalf("generated Go missing %q:\n%s", want, code) @@ -854,16 +879,9 @@ worker Counter { } ` - prog, err := Parse(src) - if err != nil { - t.Fatalf("parse failed: %v", err) - } - _, err = GenerateGo(prog) + _, err := Parse(src) 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) + t.Fatal("expected removed worker syntax error") } } @@ -1054,25 +1072,9 @@ worker Counter { } } ` - 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) - } + _, err := Parse(src) + if err == nil { + t.Fatal("expected removed worker syntax error") } } @@ -1089,7 +1091,7 @@ fun main() { if err == nil { t.Fatal("expected parse error") } - if !strings.Contains(err.Error(), "'go' expects a function call expression") { + if !strings.Contains(err.Error(), "bare go is removed") { t.Fatalf("unexpected parse error: %v", err) } } @@ -1104,9 +1106,11 @@ fun writer(ch: Channel) { fun main() { val ch = Channel() - go writer(ch) - select { - ch -> println(it) + runBlocking { + launch { writer(ch) } + select { + ch -> println(it) + } } val v = ch.read() println(v) @@ -1127,13 +1131,13 @@ fun main() { for _, want := range []string{ `func writer(ch chan int)`, `ch <- 7`, - `ch := gotlinAutoThrow(make(chan int))`, - `go func() {`, + `ch := make(chan int)`, + `gotlinScope.Launch`, `writer(ch)`, `select {`, `case it := <-ch:`, `fmt.Println(it)`, - `v := gotlinAutoThrow(<-ch)`, + `v := <-ch`, `fmt.Println(v)`, } { if !strings.Contains(code, want) { @@ -1326,7 +1330,7 @@ fun main() { } } -func TestGenerateGoAutoThrowForBunStyleCalls(t *testing.T) { +func TestGenerateGoExplicitErrorsForBunStyleCalls(t *testing.T) { src := ` package demo @@ -1356,7 +1360,7 @@ class Repo(val db: *bun.DB) { code := string(out) for _, want := range []string{ `self.db.NewSelect().ColumnExpr("1").Scan(ctx)`, - `total := gotlinAutoThrow(self.db.NewSelect().ColumnExpr("1").Count(ctx))`, + `total := self.db.NewSelect().ColumnExpr("1").Count(ctx)`, `fmt.Println(total)`, } { if !strings.Contains(code, want) { diff --git a/internal/lang/coroutines.go b/internal/lang/coroutines.go new file mode 100644 index 0000000..0ecbe9f --- /dev/null +++ b/internal/lang/coroutines.go @@ -0,0 +1,124 @@ +package lang + +import ( + "fmt" + "strings" +) + +var coroutineBuiltins = map[string]bool{"runBlocking": true, "coroutineScope": true, "launch": true, "async": true, "delay": true, "withTimeout": true, "isActive": true} + +func programUsesCoroutines(program *Program) bool { + for _, fn := range program.Functions { + if fn.Suspend || statementsUseCoroutines(fn.Body) { + return true + } + } + for _, class := range program.Classes { + for _, fn := range class.Methods { + if fn.Suspend || statementsUseCoroutines(fn.Body) { + return true + } + } + } + return false +} +func statementsUseCoroutines(stmts []Stmt) bool { + for _, stmt := range stmts { + if statementUsesCoroutines(stmt) { + return true + } + } + return false +} +func statementUsesCoroutines(stmt Stmt) bool { + switch s := stmt.(type) { + case VarDecl: + return expressionUsesCoroutines(s.Value) + case MultiVarDecl: + return expressionUsesCoroutines(s.Value) + case AssignStmt: + return expressionUsesCoroutines(s.Value) + case ExprStmt: + return expressionUsesCoroutines(s.Value) + case ReturnStmt: + return s.Value != nil && expressionUsesCoroutines(s.Value) + case IfStmt: + return expressionUsesCoroutines(s.Cond) || statementsUseCoroutines(s.Then) || statementsUseCoroutines(s.Else) + case WhileStmt: + return expressionUsesCoroutines(s.Cond) || statementsUseCoroutines(s.Body) + case ForEachStmt: + return expressionUsesCoroutines(s.Source) || statementsUseCoroutines(s.Body) + case TryCatchStmt: + return statementsUseCoroutines(s.TryBody) || statementsUseCoroutines(s.CatchBody) + } + return false +} +func expressionUsesCoroutines(expr Expr) bool { + switch e := expr.(type) { + case CallExpr: + if id, ok := e.Callee.(IdentExpr); ok && coroutineBuiltins[id.Name] { + return true + } + for _, a := range e.Args { + if expressionUsesCoroutines(a) { + return true + } + } + case LambdaExpr: + return statementsUseCoroutines(e.Body) + case SelectorExpr: + return expressionUsesCoroutines(e.Receiver) + case BinaryExpr: + return expressionUsesCoroutines(e.Left) || expressionUsesCoroutines(e.Right) + case UnaryExpr: + return expressionUsesCoroutines(e.Value) + } + return false +} + +func (g *goGenerator) emitCoroutineSupport() { + g.line("type GotlinCoroutineScope struct { ctx context.Context; cancel context.CancelFunc; workers sync.WaitGroup; mutex sync.Mutex; failure any }") + g.line("func gotlinNewCoroutineScope(parent context.Context) *GotlinCoroutineScope { ctx, cancel := context.WithCancel(parent); return &GotlinCoroutineScope{ctx: ctx, cancel: cancel} }") + g.line("func (scope *GotlinCoroutineScope) fail(value any) { scope.mutex.Lock(); if scope.failure == nil { scope.failure = value; scope.cancel() }; scope.mutex.Unlock() }") + g.line("func (scope *GotlinCoroutineScope) Launch(block func(*GotlinCoroutineScope)) { scope.workers.Add(1); go func(){ defer scope.workers.Done(); child:=gotlinNewCoroutineScope(scope.ctx); defer child.cancel(); defer func(){if value:=recover();value!=nil{scope.fail(value)}}(); block(child); child.wait() }() }") + g.line("func (scope *GotlinCoroutineScope) wait() { scope.workers.Wait(); scope.mutex.Lock(); failure:=scope.failure; scope.mutex.Unlock(); if failure!=nil{panic(failure)} }") + g.line("func (scope *GotlinCoroutineScope) Scope(block func(*GotlinCoroutineScope)) { child:=gotlinNewCoroutineScope(scope.ctx); defer child.cancel(); defer func(){if value:=recover();value!=nil{child.cancel();child.workers.Wait();panic(value)}}(); block(child); child.wait() }") + g.line("func (scope *GotlinCoroutineScope) Delay(ms int) { timer:=time.NewTimer(time.Duration(ms)*time.Millisecond); defer timer.Stop(); select { case <-timer.C: case <-scope.ctx.Done(): panic(scope.ctx.Err()) } }") + g.line("func (scope *GotlinCoroutineScope) IsActive() bool { return scope.ctx.Err()==nil }") + g.line("func (scope *GotlinCoroutineScope) WithTimeout(ms int, block func(*GotlinCoroutineScope)) { ctx,cancel:=context.WithTimeout(scope.ctx,time.Duration(ms)*time.Millisecond); defer cancel(); child:=gotlinNewCoroutineScope(ctx); defer child.cancel(); block(child); child.wait() }") + g.line("func gotlinRunBlocking(block func(*GotlinCoroutineScope)) { scope:=gotlinNewCoroutineScope(context.Background()); defer scope.cancel(); defer func(){if value:=recover();value!=nil{scope.cancel();scope.workers.Wait();panic(value)}}(); block(scope); scope.wait() }") + g.line("type GotlinDeferred[T any] struct { done chan struct{}; value T; failure any }") + g.line("func gotlinAsync[T any](scope *GotlinCoroutineScope, block func(*GotlinCoroutineScope) T) *GotlinDeferred[T] { deferred:=&GotlinDeferred[T]{done:make(chan struct{})}; scope.Launch(func(child *GotlinCoroutineScope){defer close(deferred.done);defer func(){if value:=recover();value!=nil{deferred.failure=value;scope.fail(value)}}();deferred.value=block(child)}); return deferred }") + g.line("func (deferred *GotlinDeferred[T]) await() T { <-deferred.done; if deferred.failure!=nil{panic(deferred.failure)}; return deferred.value }") +} + +func (g *goGenerator) coroutineLambda(lambda LambdaExpr, returnType string) (string, error) { + var b strings.Builder + b.WriteString("func(gotlinScope *GotlinCoroutineScope)") + if mapped := mapGoType(returnType); mapped != "" { + b.WriteString(" ") + b.WriteString(mapped) + } + b.WriteString(" {\n") + sub := goGenerator{indentLevel: 1, needsFmt: g.needsFmt, needsTime: g.needsTime, needsCoroutines: true, functions: g.functions, classes: g.classes, workers: g.workers, enums: g.enums, imports: g.imports, currentFunc: FunctionDecl{ReturnType: returnType}, currentClass: g.currentClass, currentWorker: g.currentWorker, currentCoroutineScope: "gotlinScope", mappings: g.mappings} + sub.scopes = g.cloneScopes() + sub.typeScopes = g.cloneTypeScopes() + sub.pushScope() + if err := sub.block(lambda.Body); err != nil { + return "", err + } + b.Write(sub.buf.Bytes()) + b.WriteString("}") + return b.String(), nil +} + +func coroutineLambdaArg(call CallExpr, name string) (LambdaExpr, error) { + if len(call.Args) != 1 { + return LambdaExpr{}, fmt.Errorf("%s expects one lambda", name) + } + lambda, ok := call.Args[0].(LambdaExpr) + if !ok { + return LambdaExpr{}, fmt.Errorf("%s expects a lambda", name) + } + return lambda, nil +} diff --git a/internal/lang/coroutines_test.go b/internal/lang/coroutines_test.go new file mode 100644 index 0000000..cae7eab --- /dev/null +++ b/internal/lang/coroutines_test.go @@ -0,0 +1,69 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestGenerateStructuredCoroutines(t *testing.T) { + prog, err := Parse(`package demo +suspend fun load(): Int { delay(1); return 42 } +fun main() { + runBlocking { + coroutineScope { + launch { delay(1) } + val result = async { return load() } + println(result.await()) + } + } +}`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"func load(gotlinScope *GotlinCoroutineScope) int", "gotlinScope.Delay(1)", "gotlinScope.Launch", "gotlinAsync[int]", "load(gotlinScope)", ".await()", "gotlinRunBlocking"} { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestSuspendFunctionRequiresScope(t *testing.T) { + prog, err := Parse(`package demo +suspend fun load(): Int { return 1 } +fun main() { println(load()) }`) + if err != nil { + t.Fatal(err) + } + _, err = GenerateGo(prog) + if err == nil || !strings.Contains(err.Error(), "requires a coroutine scope") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestWorkerAndBareGoAreRemoved(t *testing.T) { + for _, source := range []string{`package demo worker Counter { var count = 0 }`, `package demo fun main() { go println("x") }`} { + if _, err := Parse(source); err == nil { + t.Fatalf("deprecated concurrency syntax parsed: %s", source) + } + } +} + +func TestRunBlockingCancelsAndJoinsChildrenOnPanic(t *testing.T) { + prog, err := Parse(`package demo fun main() { runBlocking { launch { delay(1) }; panic("failed") } }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"scope.cancel()", "scope.workers.Wait()", "panic(value)", "gotlinRunBlocking"} { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} diff --git a/internal/lang/data_class_test.go b/internal/lang/data_class_test.go index 03a3f47..84f6242 100644 --- a/internal/lang/data_class_test.go +++ b/internal/lang/data_class_test.go @@ -53,7 +53,7 @@ import json encoding.json data class Request(var email: String, private val traceId: String) fun email(body: ByteSlice): String { - val request = json.decode(body) + val request = json.decode(body).unwrap() return request.email } `) diff --git a/internal/lang/enum_test.go b/internal/lang/enum_test.go index fd3de25..c36b059 100644 --- a/internal/lang/enum_test.go +++ b/internal/lang/enum_test.go @@ -49,8 +49,8 @@ fun use(result: Result) { match (result) { Result::Ok -> { println("ok") } } }`) func TestRejectWrongVariantPayloadCount(t *testing.T) { prog, err := Parse(`package demo -enum Result { Ok(String) } -fun main() { val result = Result::Ok() }`) +enum Outcome { Ok(String) } +fun main() { val result = Outcome::Ok() }`) if err != nil { t.Fatal(err) } diff --git a/internal/lang/generate_go.go b/internal/lang/generate_go.go index 5903a92..c11b976 100644 --- a/internal/lang/generate_go.go +++ b/internal/lang/generate_go.go @@ -19,6 +19,7 @@ func GenerateGoMain(program *Program) ([]byte, error) { } func generateGo(program *Program, packageOverride string) ([]byte, error) { + normalizeClassReferences(program) if err := validateMutability(program); err != nil { return nil, err } @@ -39,10 +40,11 @@ type goGenerator struct { indentLevel int needsFmt bool needsRunCatch bool - needsAutoThrow bool + needsGoUnwrap bool needsTime bool needsEveryMs bool needsJSONDecode bool + needsCoroutines bool sqlContextAlias string sqlPGXAlias string functions map[string]FunctionDecl @@ -54,9 +56,11 @@ type goGenerator struct { currentClass *ClassDecl currentWorker *WorkerDecl currentWorkerFieldReceiver string + currentCoroutineScope string scopes []map[string]bool typeScopes []map[string]string matchCounter int + resultCounter int mappings *mappingState } @@ -64,8 +68,12 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { g.mappings = &mappingState{functions: map[string]string{}} containsSQL := programContainsSQL(program) containsSQLExecution := programContainsSQLExecution(program) + g.needsCoroutines = programUsesCoroutines(program) + if g.needsCoroutines { + g.needsTime = true + } if containsSQLExecution { - g.needsAutoThrow = true + g.needsGoUnwrap = true g.sqlContextAlias = runtimeImportAlias(program, "context", "gotlincontext") g.sqlPGXAlias = runtimeImportAlias(program, "github.com/jackc/pgx/v5", "gotlinpgx") } @@ -142,36 +150,36 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { } } } - if !g.needsAutoThrow { + if !g.needsGoUnwrap { for _, fn := range program.Functions { - if usesAutoThrow(fn.Body) { - g.needsAutoThrow = true + if usesGoUnwrap(fn.Body) { + g.needsGoUnwrap = true break } } } - if !g.needsAutoThrow { + if !g.needsGoUnwrap { for _, class := range program.Classes { for _, method := range class.Methods { - if usesAutoThrow(method.Body) { - g.needsAutoThrow = true + if usesGoUnwrap(method.Body) { + g.needsGoUnwrap = true break } } - if g.needsAutoThrow { + if g.needsGoUnwrap { break } } } - if !g.needsAutoThrow { + if !g.needsGoUnwrap { for _, worker := range program.Workers { for _, method := range worker.Methods { - if usesAutoThrow(method.Body) { - g.needsAutoThrow = true + if usesGoUnwrap(method.Body) { + g.needsGoUnwrap = true break } } - if g.needsAutoThrow { + if g.needsGoUnwrap { break } } @@ -257,12 +265,14 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { } } } - var runtimeImports map[string]string + runtimeImports := map[string]string{} if containsSQLExecution { - runtimeImports = map[string]string{ - "context": g.sqlContextAlias, - "github.com/jackc/pgx/v5": g.sqlPGXAlias, - } + runtimeImports["context"] = g.sqlContextAlias + runtimeImports["github.com/jackc/pgx/v5"] = g.sqlPGXAlias + } + if g.needsCoroutines { + runtimeImports["context"] = "context" + runtimeImports["sync"] = "sync" } imports := collectImports(program, g.needsFmt, g.needsTime, runtimeImports) if len(program.Embeds) > 0 { @@ -296,14 +306,22 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { g.emitRunCatchingSupport() g.line("") } - if g.needsAutoThrow { - g.emitAutoThrowSupport() + if g.needsGoUnwrap { + g.emitGoUnwrapSupport() g.line("") } if g.needsEveryMs { g.emitEveryMsSupport() g.line("") } + if g.needsCoroutines { + g.emitCoroutineSupport() + g.line("") + } + g.line("type GotlinResult[T any] struct { Value T; Err error }") + g.line("func gotlinResultUnwrap[T any](result GotlinResult[T]) T { if result.Err != nil { panic(result.Err) }; return result.Value }") + g.line("func gotlinResultUnwrapOr[T any](result GotlinResult[T], fallback T) T { if result.Err != nil { return fallback }; return result.Value }") + g.line("") for _, embedded := range program.Embeds { g.line("//go:embed " + embedded.Path) g.line("var " + embedded.Name + " " + mapGoType(embedded.Type)) @@ -375,6 +393,11 @@ func (g *goGenerator) function(fn FunctionDecl) error { g.currentClass = nil g.currentWorker = nil g.currentFunc = fn + previousScope := g.currentCoroutineScope + if fn.Suspend { + g.currentCoroutineScope = "gotlinScope" + } + defer func() { g.currentCoroutineScope = previousScope }() g.scopes = nil g.pushScope() for _, param := range fn.Params { @@ -382,7 +405,7 @@ func (g *goGenerator) function(fn FunctionDecl) error { } g.write("func ") g.write(fn.Name) - g.write(renderGoParams(fn.Params)) + g.write(renderGoFunctionParams(fn)) if ret := mapGoReturnType(fn.ReturnType); ret != "" { g.write(" ") g.write(ret) @@ -402,7 +425,11 @@ 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)) + prefix := "" + if method.Suspend { + prefix = "gotlinScope *GotlinCoroutineScope" + } + g.line(method.Name + renderGoParamsWithPrefix(method.Params, prefix) + renderGoReturnSuffix(method.ReturnType)) } g.indentLevel-- g.line("}") @@ -569,7 +596,7 @@ func (g *goGenerator) workerDecl(worker WorkerDecl) error { if err != nil { return err } - value = g.autoThrowValue(field.Value, value) + value = g.passthroughValue(field.Value, value) g.line("state." + field.Name + " = " + value) } g.line("go func() {") @@ -597,6 +624,11 @@ func (g *goGenerator) workerDecl(worker WorkerDecl) error { func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error { g.currentClass = &class g.currentFunc = fn + previousScope := g.currentCoroutineScope + if fn.Suspend { + g.currentCoroutineScope = "gotlinScope" + } + defer func() { g.currentCoroutineScope = previousScope }() g.scopes = nil g.pushScope() g.define("self") @@ -609,7 +641,7 @@ func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error { g.write(class.Name) g.write(") ") g.write(fn.Name) - g.write(renderGoParams(fn.Params)) + g.write(renderGoFunctionParams(fn)) if ret := mapGoReturnType(fn.ReturnType); ret != "" { g.write(" ") g.write(ret) @@ -643,7 +675,7 @@ func (g *goGenerator) workerMethod(worker WorkerDecl, fn FunctionDecl) error { g.write(worker.Name) g.write(") ") g.write(fn.Name) - g.write(renderGoParams(fn.Params)) + g.write(renderGoFunctionParams(fn)) if ret := mapGoReturnType(fn.ReturnType); ret != "" { g.write(" ") g.write(ret) @@ -740,14 +772,58 @@ func (g *goGenerator) block(stmts []Stmt) error { return nil } +func (g *goGenerator) tryVarDecl(decl VarDecl, attempt TryExpr, tail []Stmt) error { + base, resultArgs, ok := parseGenericType(g.currentFunc.ReturnType) + if !ok || base != "Result" || len(resultArgs) != 2 { + return fmt.Errorf("? can only be used inside a function returning Result") + } + g.resultCounter++ + errorName := fmt.Sprintf("gotlinError%d", g.resultCounter) + valueName := decl.Name + if !nameUsedInStmts(decl.Name, tail) { + valueName = "_" + } + innerType := g.exprType(attempt.Value) + if innerBase, innerArgs, isResult := parseGenericType(innerType); isResult && innerBase == "Result" && len(innerArgs) == 2 { + resultName := fmt.Sprintf("gotlinResult%d", g.resultCounter) + value, err := g.expr(attempt.Value, innerType) + if err != nil { + return err + } + g.line(resultName + " := " + value) + g.line("if " + resultName + ".Err != nil { return GotlinResult[" + mapGoType(resultArgs[0]) + "]{Err: " + resultName + ".Err} }") + g.line(valueName + " := " + resultName + ".Value") + if decl.Type == "" { + decl.Type = innerArgs[0] + } + } else { + value, err := g.expr(attempt.Value, "") + if err != nil { + return err + } + g.line(valueName + ", " + errorName + " := " + value) + g.line("if " + errorName + " != nil { return GotlinResult[" + mapGoType(resultArgs[0]) + "]{Err: " + errorName + "} }") + } + if valueName != "_" { + g.defineType(decl.Name, decl.Type) + } + return nil +} + func (g *goGenerator) stmt(stmt Stmt, tail []Stmt) error { switch s := stmt.(type) { case VarDecl: + if _, isNull := s.Value.(NullExpr); isNull && s.Type == "" { + return fmt.Errorf("null requires an explicit nullable type") + } + if attempt, ok := s.Value.(TryExpr); ok { + return g.tryVarDecl(s, attempt, tail) + } value, err := g.expr(s.Value, s.Type) if err != nil { return err } - value = g.autoThrowValue(s.Value, value) + value = g.passthroughValue(s.Value, value) if !nameUsedInStmts(s.Name, tail) { g.line(fmt.Sprintf("_ = %s", value)) return nil @@ -788,18 +864,18 @@ func (g *goGenerator) stmt(stmt Stmt, tail []Stmt) error { } } case AssignStmt: - value, err := g.expr(s.Value, "") + value, err := g.expr(s.Value, g.lookupType(s.Name)) if err != nil { return err } - value = g.autoThrowValue(s.Value, value) + value = g.passthroughValue(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) + value = g.passthroughValue(s.Value, value) g.line(fmt.Sprintf("%s += %s", g.assignTarget(s.Name), value)) case MultiAssignStmt: value, err := g.expr(s.Value, "") @@ -819,7 +895,7 @@ func (g *goGenerator) stmt(stmt Stmt, tail []Stmt) error { if err != nil { return err } - value = g.autoThrowValue(s.Value, value) + value = g.passthroughValue(s.Value, value) g.line(fmt.Sprintf("return %s", value)) } case ThrowStmt: @@ -868,6 +944,11 @@ func (g *goGenerator) stmt(stmt Stmt, tail []Stmt) error { g.write(fmt.Sprintf("if %s {\n", cond)) g.indentLevel++ g.pushScope() + if name, thenNonNull, _ := nullableCondition(s.Cond); thenNonNull { + if typ := g.lookupType(name); strings.HasSuffix(typ, "?") { + g.defineType(name, strings.TrimSuffix(typ, "?")) + } + } if err := g.block(s.Then); err != nil { return err } @@ -879,6 +960,11 @@ func (g *goGenerator) stmt(stmt Stmt, tail []Stmt) error { g.write(" else {\n") g.indentLevel++ g.pushScope() + if name, _, elseNonNull := nullableCondition(s.Cond); elseNonNull { + if typ := g.lookupType(name); strings.HasSuffix(typ, "?") { + g.defineType(name, strings.TrimSuffix(typ, "?")) + } + } if err := g.block(s.Else); err != nil { return err } @@ -888,6 +974,13 @@ func (g *goGenerator) stmt(stmt Stmt, tail []Stmt) error { g.write("}") } g.write("\n") + if len(s.Else) == 0 && blockAlwaysTerminates(s.Then) { + if name, _, elseNonNull := nullableCondition(s.Cond); elseNonNull { + if typ := g.lookupType(name); strings.HasSuffix(typ, "?") { + g.defineType(name, strings.TrimSuffix(typ, "?")) + } + } + } case WhileStmt: cond, err := g.expr(s.Cond, "") if err != nil { @@ -1068,6 +1161,11 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { if lowered, handled, err := g.lowerSQLQuery(expr); handled || err != nil { return lowered, err } + if expectedType != "" && expectedType != "Any" { + if actualType := g.exprType(expr); strings.HasSuffix(actualType, "?") && !strings.HasSuffix(expectedType, "?") { + return "", fmt.Errorf("nullable value of type %s cannot be used as non-nullable %s; use ?. or !!", actualType, expectedType) + } + } switch e := expr.(type) { case IdentExpr: if g.currentClass != nil { @@ -1103,7 +1201,26 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } return "false", nil case NullExpr: + if expectedType != "" && !strings.HasSuffix(expectedType, "?") { + return "", fmt.Errorf("null is not allowed for non-nullable type %s", expectedType) + } return "nil", nil + case NonNullExpr: + sourceType := g.exprType(e.Value) + if !strings.HasSuffix(sourceType, "?") { + return "", fmt.Errorf("non-null assertion requires a nullable value, got %s", sourceType) + } + innerType := strings.TrimSuffix(sourceType, "?") + value, err := g.expr(e.Value, sourceType) + if err != nil { + return "", err + } + if strings.HasPrefix(innerType, "*") { + return fmt.Sprintf("func(value %s) %s { if value == nil { panic(\"non-null assertion failed\") }; return value }(%s)", mapGoType(sourceType), mapGoType(innerType), value), nil + } + return fmt.Sprintf("func(value %s) %s { if value == nil { panic(\"non-null assertion failed\") }; return *value }(%s)", mapGoType(sourceType), mapGoType(innerType), value), nil + case TryExpr: + return "", fmt.Errorf("? propagation is only supported on variable declarations") case UnaryExpr: value, err := g.wrapExpr(e.Value, "") if err != nil { @@ -1111,6 +1228,18 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } return e.Op + value, nil case BinaryExpr: + if e.Op == "==" || e.Op == "!=" { + if _, ok := e.Left.(NullExpr); ok { + if typ := g.exprType(e.Right); typ != "" && !strings.HasSuffix(typ, "?") { + return "", fmt.Errorf("null comparison requires a nullable value, got %s", typ) + } + } + if _, ok := e.Right.(NullExpr); ok { + if typ := g.exprType(e.Left); typ != "" && !strings.HasSuffix(typ, "?") { + return "", fmt.Errorf("null comparison requires a nullable value, got %s", typ) + } + } + } left, err := g.wrapExpr(e.Left, "") if err != nil { return "", err @@ -1121,6 +1250,162 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } return fmt.Sprintf("%s %s %s", left, e.Op, right), nil case CallExpr: + if ident, ok := e.Callee.(IdentExpr); ok && coroutineBuiltins[ident.Name] { + switch ident.Name { + case "runBlocking": + lambda, err := coroutineLambdaArg(e, "runBlocking") + if err != nil { + return "", err + } + block, err := g.coroutineLambda(lambda, "Unit") + if err != nil { + return "", err + } + return "gotlinRunBlocking(" + block + ")", nil + case "coroutineScope", "launch": + if g.currentCoroutineScope == "" { + return "", fmt.Errorf("%s requires a coroutine scope", ident.Name) + } + lambda, err := coroutineLambdaArg(e, ident.Name) + if err != nil { + return "", err + } + block, err := g.coroutineLambda(lambda, "Unit") + if err != nil { + return "", err + } + method := "Scope" + if ident.Name == "launch" { + method = "Launch" + } + return g.currentCoroutineScope + "." + method + "(" + block + ")", nil + case "async": + if g.currentCoroutineScope == "" { + return "", fmt.Errorf("async requires a coroutine scope") + } + if len(e.TypeArgs) != 1 { + return "", fmt.Errorf("async expects one result type") + } + lambda, err := coroutineLambdaArg(e, "async") + if err != nil { + return "", err + } + block, err := g.coroutineLambda(lambda, e.TypeArgs[0]) + if err != nil { + return "", err + } + return "gotlinAsync[" + mapGoType(e.TypeArgs[0]) + "](" + g.currentCoroutineScope + ", " + block + ")", nil + case "delay": + if g.currentCoroutineScope == "" || len(e.Args) != 1 { + return "", fmt.Errorf("delay(ms) requires a coroutine scope") + } + ms, err := g.expr(e.Args[0], "Int") + if err != nil { + return "", err + } + return g.currentCoroutineScope + ".Delay(" + ms + ")", nil + case "withTimeout": + if g.currentCoroutineScope == "" || len(e.Args) != 2 { + return "", fmt.Errorf("withTimeout expects milliseconds and a lambda") + } + ms, err := g.expr(e.Args[0], "Int") + if err != nil { + return "", err + } + lambda, ok := e.Args[1].(LambdaExpr) + if !ok { + return "", fmt.Errorf("withTimeout expects a lambda") + } + block, err := g.coroutineLambda(lambda, "Unit") + if err != nil { + return "", err + } + return g.currentCoroutineScope + ".WithTimeout(" + ms + ", " + block + ")", nil + case "isActive": + if g.currentCoroutineScope == "" { + return "", fmt.Errorf("isActive requires a coroutine scope") + } + return g.currentCoroutineScope + ".IsActive()", nil + } + } + if selector, ok := e.Callee.(SelectorExpr); ok && selector.Name == "await" && len(e.Args) == 0 { + receiver, err := g.expr(selector.Receiver, "") + if err != nil { + return "", err + } + return receiver + ".await()", nil + } + if selector, ok := e.Callee.(SelectorExpr); ok && selector.Name == "unwrap" && len(e.Args) == 0 { + receiverType := g.exprType(selector.Receiver) + receiver, err := g.expr(selector.Receiver, receiverType) + if err != nil { + return "", err + } + if base, _, ok := parseGenericType(receiverType); ok && base == "Result" { + return "gotlinResultUnwrap(" + receiver + ")", nil + } + return "gotlinUnwrapGo(" + receiver + ")", nil + } + if selector, ok := e.Callee.(SelectorExpr); ok && selector.Name == "unwrapOr" && len(e.Args) == 1 { + receiverType := g.exprType(selector.Receiver) + base, args, ok := parseGenericType(receiverType) + if !ok || base != "Result" || len(args) != 2 { + return "", fmt.Errorf("unwrapOr requires a Result value") + } + receiver, err := g.expr(selector.Receiver, receiverType) + if err != nil { + return "", err + } + fallback, err := g.expr(e.Args[0], args[0]) + if err != nil { + return "", err + } + return "gotlinResultUnwrapOr(" + receiver + ", " + fallback + ")", nil + } + if safe, ok := e.Callee.(SafeSelectorExpr); ok { + receiverType := g.exprType(safe.Receiver) + if !strings.HasSuffix(receiverType, "?") { + return "", fmt.Errorf("safe call requires a nullable receiver, got %s", receiverType) + } + innerType := strings.TrimSuffix(receiverType, "?") + class, ok := g.classForType(innerType) + if !ok { + return "", fmt.Errorf("safe call receiver %s is not a Gotlin class", receiverType) + } + var method *FunctionDecl + for i := range class.Methods { + if class.Methods[i].Name == safe.Name { + method = &class.Methods[i] + break + } + } + if method == nil { + return "", fmt.Errorf("unknown safe-call method %s.%s", class.Name, safe.Name) + } + if len(e.Args) != len(method.Params) { + return "", fmt.Errorf("%s.%s expects %d arguments", class.Name, safe.Name, len(method.Params)) + } + receiver, err := g.expr(safe.Receiver, receiverType) + if err != nil { + return "", err + } + args := make([]string, 0, len(e.Args)) + for i, arg := range e.Args { + value, err := g.expr(arg, method.Params[i].Type) + if err != nil { + return "", err + } + args = append(args, value) + } + call := "value." + safe.Name + "(" + strings.Join(args, ", ") + ")" + if method.ReturnType == "Unit" { + return fmt.Sprintf("func(value %s) { if value != nil { %s } }(%s)", mapGoType(receiverType), call, receiver), nil + } + if strings.HasPrefix(method.ReturnType, "*") || strings.HasSuffix(method.ReturnType, "?") { + return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; return %s }(%s)", mapGoType(receiverType), mapGoType(nullableType(method.ReturnType)), call, receiver), nil + } + return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; result := %s; return &result }(%s)", mapGoType(receiverType), mapGoType(nullableType(method.ReturnType)), call, receiver), nil + } if selector, ok := e.Callee.(SelectorExpr); ok && selector.Name == "mapTo" { if len(e.TypeArgs) > 1 || len(e.Args) != 0 || len(e.NamedArgs) != 0 { return "", fmt.Errorf("mapTo expects at most one type argument and no value arguments") @@ -1179,12 +1464,36 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } if ident, ok := e.Callee.(IdentExpr); ok { switch ident.Name { + case "goAssert": + if len(e.TypeArgs) != 1 || len(e.Args) != 1 { + return "", fmt.Errorf("goAssert(value) expects one type and one value") + } + value, err := g.expr(e.Args[0], "") + if err != nil { + return "", err + } + return "(" + value + ").(" + mapGoType(e.TypeArgs[0]) + ")", nil 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 "keys": + if len(e.Args) != 1 { + return "", fmt.Errorf("keys(map) expects exactly one argument") + } + _, mapTypes, ok := parseGenericType(g.exprType(e.Args[0])) + if !ok || len(mapTypes) != 2 { + return "", fmt.Errorf("keys(map) requires a typed map") + } + value, err := g.expr(e.Args[0], "") + if err != nil { + return "", err + } + keyType := mapGoType(mapTypes[0]) + mapType := mapGoType(g.exprType(e.Args[0])) + return fmt.Sprintf("func(values %s) []%s { result := make([]%s, 0, len(values)); for key := range values { result = append(result, key) }; return result }(%s)", mapType, keyType, keyType, value), nil case "after", "every": if lowered, handled, err := g.lowerTimerSourceCall(e); handled || err != nil { return lowered, err @@ -1245,6 +1554,9 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { if len(args) != 1 { return "", fmt.Errorf("ByteSlice expects exactly one argument") } + if g.exprType(e.Args[0]) == "Int" { + return "make([]byte, " + args[0] + ")", nil + } return "[]byte(" + args[0] + ")", nil } if ident, ok := e.Callee.(IdentExpr); ok && ident.Name == "runCatching" { @@ -1264,12 +1576,40 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { return fmt.Sprintf("New%s()", ident.Name), nil } } + if ident, ok := e.Callee.(IdentExpr); ok { + if fn, found := g.functions[ident.Name]; found && fn.Suspend { + if g.currentCoroutineScope == "" { + return "", fmt.Errorf("suspend function %s requires a coroutine scope", ident.Name) + } + args = append([]string{g.currentCoroutineScope}, args...) + } + } + if selector, ok := e.Callee.(SelectorExpr); ok { + if class, found := g.classForType(g.exprType(selector.Receiver)); found { + for _, method := range class.Methods { + if method.Name == selector.Name && method.Suspend { + if g.currentCoroutineScope == "" { + return "", fmt.Errorf("suspend method %s requires a coroutine scope", selector.Name) + } + args = append([]string{g.currentCoroutineScope}, args...) + break + } + } + } + } callee, err := g.expr(e.Callee, "") if err != nil { return "", err } - return fmt.Sprintf("%s(%s)", callee, strings.Join(args, ", ")), nil + call := fmt.Sprintf("%s(%s)", callee, strings.Join(args, ", ")) + if result, ok := g.goErrorResult(e, call, expectedType); ok { + return result, nil + } + return call, nil case SelectorExpr: + if receiverType := g.exprType(e.Receiver); strings.HasSuffix(receiverType, "?") { + return "", fmt.Errorf("nullable receiver %s requires ?. or !! before .%s", receiverType, e.Name) + } receiver, err := g.expr(e.Receiver, "") if err != nil { return "", err @@ -1306,6 +1646,29 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { name = exportedGoName(name) } return fmt.Sprintf("%s.%s", receiver, name), nil + case SafeSelectorExpr: + receiverType := g.exprType(e.Receiver) + if !strings.HasSuffix(receiverType, "?") { + return "", fmt.Errorf("safe access requires a nullable receiver, got %s", receiverType) + } + innerType := strings.TrimSuffix(receiverType, "?") + class, ok := g.classForType(innerType) + if !ok { + return "", fmt.Errorf("safe access receiver %s is not a Gotlin class", receiverType) + } + field, ok := classFieldByName(class, e.Name) + if !ok { + return "", fmt.Errorf("unknown field %s.%s", class.Name, e.Name) + } + receiver, err := g.expr(e.Receiver, receiverType) + if err != nil { + return "", err + } + fieldName := mappingFieldName(class, field) + if strings.HasPrefix(field.Type, "*") || strings.HasSuffix(field.Type, "?") { + return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; return value.%s }(%s)", mapGoType(receiverType), mapGoType(nullableType(field.Type)), fieldName, receiver), nil + } + return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; result := value.%s; return &result }(%s)", mapGoType(receiverType), mapGoType(nullableType(field.Type)), fieldName, receiver), nil case IndexExpr: receiver, err := g.expr(e.Receiver, "") if err != nil { @@ -1317,6 +1680,33 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } return receiver + "[" + index + "]", nil case EnumVariantExpr: + if e.EnumName == "Result" { + base, args, ok := parseGenericType(expectedType) + if !ok || base != "Result" || len(args) != 2 { + return "", fmt.Errorf("Result::%s requires an expected Result type", e.VariantName) + } + if e.VariantName == "Ok" { + if len(e.Values) != 1 { + return "", fmt.Errorf("Result::Ok expects one value") + } + value, err := g.expr(e.Values[0], args[0]) + if err != nil { + return "", err + } + return fmt.Sprintf("GotlinResult[%s]{Value: %s}", mapGoType(args[0]), value), nil + } + if e.VariantName == "Err" { + if len(e.Values) != 1 { + return "", fmt.Errorf("Result::Err expects one error") + } + value, err := g.expr(e.Values[0], "Error") + if err != nil { + return "", err + } + return fmt.Sprintf("GotlinResult[%s]{Err: %s}", mapGoType(args[0]), value), nil + } + return "", fmt.Errorf("unknown Result variant %s", e.VariantName) + } decl, ok := g.enums[e.EnumName] if !ok { return "", fmt.Errorf("unknown enum %s", e.EnumName) @@ -1354,11 +1744,11 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } func (g *goGenerator) emitJSONDecodeSupport() { - g.line("func gotlinJSONDecode[T any](body []byte) T {") + g.line("func gotlinJSONDecode[T any](body []byte) GotlinResult[T] {") g.indentLevel++ g.line("var value T") - g.line("gotlinAutoThrow(json.Unmarshal(body, &value))") - g.line("return value") + g.line("if err := json.Unmarshal(body, &value); err != nil { return GotlinResult[T]{Err: err} }") + g.line("return GotlinResult[T]{Value: value}") g.indentLevel-- g.line("}") } @@ -1396,40 +1786,44 @@ func (g *goGenerator) emitSQLSupport(program *Program, execution bool) { g.indentLevel-- g.line("}") g.line("") - g.line("func gotlinSQLFetch[T any](pool gotlinSQLQuerier, ctx " + g.sqlContextAlias + ".Context, query GotlinSQLQuery, scan gotlinSQLScanner[T]) []*T {") + g.line("func gotlinSQLFetch[T any](pool gotlinSQLQuerier, ctx " + g.sqlContextAlias + ".Context, query GotlinSQLQuery, scan gotlinSQLScanner[T]) GotlinResult[[]*T] {") g.indentLevel++ - g.line("rows := gotlinAutoThrow(pool.Query(ctx, query.SQL, query.Args...))") + g.line("rows, err := pool.Query(ctx, query.SQL, query.Args...)") + g.line("if err != nil { return GotlinResult[[]*T]{Err: err} }") g.line("defer rows.Close()") g.line("values := make([]*T, 0)") g.line("for rows.Next() {") g.indentLevel++ - g.line("values = append(values, gotlinAutoThrow(scan(rows)))") + g.line("value, err := scan(rows)") + g.line("if err != nil { return GotlinResult[[]*T]{Err: err} }") + g.line("values = append(values, value)") g.indentLevel-- g.line("}") - g.line("gotlinAutoThrow(rows.Err())") - g.line("return values") + g.line("if err := rows.Err(); err != nil { return GotlinResult[[]*T]{Err: err} }") + g.line("return GotlinResult[[]*T]{Value: values}") g.indentLevel-- g.line("}") g.line("") - g.line("func gotlinSQLSingle[T any](pool gotlinSQLQuerier, ctx " + g.sqlContextAlias + ".Context, query GotlinSQLQuery, scan gotlinSQLScanner[T]) *T {") + g.line("func gotlinSQLSingle[T any](pool gotlinSQLQuerier, ctx " + g.sqlContextAlias + ".Context, query GotlinSQLQuery, scan gotlinSQLScanner[T]) GotlinResult[*T] {") g.indentLevel++ - g.line("rows := gotlinAutoThrow(pool.Query(ctx, query.SQL, query.Args...))") + g.line("rows, err := pool.Query(ctx, query.SQL, query.Args...)") + g.line("if err != nil { return GotlinResult[*T]{Err: err} }") g.line("defer rows.Close()") g.line("if !rows.Next() {") g.indentLevel++ - g.line("gotlinAutoThrow(rows.Err())") - g.line(`gotlinAutoThrow(gotlinSQLError("SQL single() expected exactly one row, got zero"))`) - g.line("return nil") + g.line("if err := rows.Err(); err != nil { return GotlinResult[*T]{Err: err} }") + g.line(`return GotlinResult[*T]{Err: gotlinSQLError("SQL single() expected exactly one row, got zero")}`) g.indentLevel-- g.line("}") - g.line("value := gotlinAutoThrow(scan(rows))") + g.line("value, err := scan(rows)") + g.line("if err != nil { return GotlinResult[*T]{Err: err} }") g.line("if rows.Next() {") g.indentLevel++ - g.line(`gotlinAutoThrow(gotlinSQLError("SQL single() expected exactly one row, got more than one"))`) + g.line(`return GotlinResult[*T]{Err: gotlinSQLError("SQL single() expected exactly one row, got more than one")}`) g.indentLevel-- g.line("}") - g.line("gotlinAutoThrow(rows.Err())") - g.line("return value") + g.line("if err := rows.Err(); err != nil { return GotlinResult[*T]{Err: err} }") + g.line("return GotlinResult[*T]{Value: value}") g.indentLevel-- g.line("}") g.line("") @@ -1442,9 +1836,11 @@ func (g *goGenerator) emitSQLSupport(program *Program, execution bool) { g.indentLevel-- g.line("}") g.line("") - g.line("func gotlinSQLIterate[T any](pool gotlinSQLQuerier, ctx " + g.sqlContextAlias + ".Context, query GotlinSQLQuery, scan gotlinSQLScanner[T]) *GotlinSQLIterator[T] {") + g.line("func gotlinSQLIterate[T any](pool gotlinSQLQuerier, ctx " + g.sqlContextAlias + ".Context, query GotlinSQLQuery, scan gotlinSQLScanner[T]) GotlinResult[*GotlinSQLIterator[T]] {") g.indentLevel++ - g.line("return &GotlinSQLIterator[T]{rows: gotlinAutoThrow(pool.Query(ctx, query.SQL, query.Args...)), scan: scan}") + g.line("rows, err := pool.Query(ctx, query.SQL, query.Args...)") + g.line("if err != nil { return GotlinResult[*GotlinSQLIterator[T]]{Err: err} }") + g.line("return GotlinResult[*GotlinSQLIterator[T]]{Value: &GotlinSQLIterator[T]{rows: rows, scan: scan}}") g.indentLevel-- g.line("}") g.line("") @@ -1465,7 +1861,7 @@ func (g *goGenerator) emitSQLSupport(program *Program, execution bool) { g.line("if err != nil {") g.indentLevel++ g.line("iterator.streamErr = err") - g.line("gotlinAutoThrow(err)") + g.line("return false") g.indentLevel-- g.line("}") g.line("iterator.current = value") @@ -1542,7 +1938,7 @@ func exportedGoName(name string) string { func exportedGoFieldName(name string) string { name = exportedGoName(name) - for _, pair := range [][2]string{{"Jwks", "JWKS"}, {"Jwt", "JWT"}, {"Url", "URL"}, {"Http", "HTTP"}, {"Https", "HTTPS"}, {"Api", "API"}, {"Sql", "SQL"}, {"Id", "ID"}} { + for _, pair := range [][2]string{{"Jwks", "JWKS"}, {"Jwt", "JWT"}, {"Url", "URL"}, {"Api", "API"}, {"Sql", "SQL"}, {"Id", "ID"}} { name = strings.ReplaceAll(name, pair[0], pair[1]) } return name @@ -1753,7 +2149,14 @@ func (g *goGenerator) write(text string) { func mapGoType(name string) string { if strings.HasSuffix(name, "?") { - return "*" + mapGoType(strings.TrimSuffix(name, "?")) + inner := strings.TrimSuffix(name, "?") + if inner == "Error" || inner == "Any" { + return mapGoType(inner) + } + if strings.HasPrefix(inner, "*") { + return mapGoType(inner) + } + return "*" + mapGoType(inner) } if params, ret, ok := parseFunctionType(name); ok { var goParams []string @@ -1785,6 +2188,13 @@ func mapGoType(name string) string { if len(mapped) == 1 { return "chan " + mapped[0] } + case "Result": + if len(mapped) == 2 { + if mapped[0] == "" { + mapped[0] = "struct{}" + } + return "GotlinResult[" + mapped[0] + "]" + } } return base + "[" + strings.Join(mapped, ", ") + "]" } @@ -1792,6 +2202,8 @@ func mapGoType(name string) string { switch name { case "Int": return "int" + case "Long": + return "int64" case "Float", "Double": return "float64" case "String": @@ -1804,11 +2216,54 @@ func mapGoType(name string) string { return "bool" case "Unit": return "" + case "Error": + return "error" default: return name } } +func nullableType(name string) string { + if strings.HasSuffix(name, "?") { + return name + } + return name + "?" +} + +func nullableCondition(expr Expr) (name string, thenNonNull bool, elseNonNull bool) { + binary, ok := expr.(BinaryExpr) + if !ok || (binary.Op != "==" && binary.Op != "!=") { + return "", false, false + } + ident, leftIdent := binary.Left.(IdentExpr) + _, rightNull := binary.Right.(NullExpr) + if !leftIdent || !rightNull { + ident, leftIdent = binary.Right.(IdentExpr) + _, rightNull = binary.Left.(NullExpr) + } + if !leftIdent || !rightNull { + return "", false, false + } + if binary.Op == "!=" { + return ident.Name, true, false + } + return ident.Name, false, true +} + +func blockAlwaysTerminates(stmts []Stmt) bool { + if len(stmts) == 0 { + return false + } + switch last := stmts[len(stmts)-1].(type) { + case ReturnStmt, ThrowStmt: + return true + case IfStmt: + return len(last.Else) > 0 && blockAlwaysTerminates(last.Then) && blockAlwaysTerminates(last.Else) + default: + return false + } +} + func mapGoReturnType(name string) string { return mapGoType(name) } @@ -1957,7 +2412,7 @@ func usesRunCatching(stmts []Stmt) bool { return false } -func usesAutoThrow(stmts []Stmt) bool { +func usesGoUnwrap(stmts []Stmt) bool { for _, stmt := range stmts { switch s := stmt.(type) { case VarDecl: @@ -1979,21 +2434,21 @@ func usesAutoThrow(stmts []Stmt) bool { } } case IfStmt: - if usesAutoThrow(s.Then) || usesAutoThrow(s.Else) { + if usesGoUnwrap(s.Then) || usesGoUnwrap(s.Else) { return true } case WhileStmt: - if usesAutoThrow(s.Body) { + if usesGoUnwrap(s.Body) { return true } case SelectStmt: for _, c := range s.Cases { - if usesAutoThrow(c.Body) { + if usesGoUnwrap(c.Body) { return true } } case TryCatchStmt: - if usesAutoThrow(s.TryBody) || usesAutoThrow(s.CatchBody) { + if usesGoUnwrap(s.TryBody) || usesGoUnwrap(s.CatchBody) { return true } } @@ -2254,6 +2709,8 @@ func (g *goGenerator) callArgTypes(callee Expr, argCount int) []string { switch selector { case "http.HandleFunc": return []string{"String", "(http.ResponseWriter, *http.Request) -> Unit"} + case "sort.Slice", "sort.slice": + return []string{"Any", "(Int, Int) -> Boolean"} default: return nil } @@ -2396,6 +2853,10 @@ func exprUsesName(expr Expr, name string, shadowed bool) bool { return !shadowed && e.Name == name case UnaryExpr: return exprUsesName(e.Value, name, shadowed) + case NonNullExpr: + return exprUsesName(e.Value, name, shadowed) + case TryExpr: + return exprUsesName(e.Value, name, shadowed) case BinaryExpr: return exprUsesName(e.Left, name, shadowed) || exprUsesName(e.Right, name, shadowed) case CallExpr: @@ -2506,6 +2967,16 @@ func (g *goGenerator) exprType(expr Expr) string { case BoolExpr: return "Boolean" case CallExpr: + if ident, ok := e.Callee.(IdentExpr); ok && ident.Name == "keys" && len(e.Args) == 1 { + if _, args, ok := parseGenericType(g.exprType(e.Args[0])); ok && len(args) == 2 { + return "List<" + args[0] + ">" + } + } + if selector, ok := e.Callee.(SelectorExpr); ok && (selector.Name == "unwrap" || selector.Name == "unwrapOr") { + if base, args, ok := parseGenericType(g.exprType(selector.Receiver)); ok && base == "Result" && len(args) == 2 { + return args[0] + } + } if selector, ok := e.Callee.(SelectorExpr); ok && selector.Name == "mapTo" && len(e.TypeArgs) == 1 { return g.mappingTopLevelTarget(e.TypeArgs[0]) } @@ -2526,7 +2997,7 @@ func (g *goGenerator) exprType(expr Expr) string { } } if receiver, ok := selector.Receiver.(IdentExpr); ok && receiver.Name == "json" && selector.Name == "decode" && len(e.TypeArgs) == 1 { - return e.TypeArgs[0] + return "Result<" + e.TypeArgs[0] + ", Error>" } if class, ok := g.classForType(g.exprType(selector.Receiver)); ok { for _, method := range class.Methods { @@ -2572,10 +3043,70 @@ func (g *goGenerator) exprType(expr Expr) string { } case EnumVariantExpr: return e.EnumName + case NonNullExpr: + return strings.TrimSuffix(g.exprType(e.Value), "?") + case TryExpr: + if base, args, ok := parseGenericType(g.exprType(e.Value)); ok && base == "Result" && len(args) == 2 { + return args[0] + } + case SafeSelectorExpr: + inner := strings.TrimSuffix(g.exprType(e.Receiver), "?") + if class, ok := g.classForType(inner); ok { + if field, found := classFieldByName(class, e.Name); found { + return nullableType(field.Type) + } + for _, method := range class.Methods { + if method.Name == e.Name { + return nullableType(method.ReturnType) + } + } + } } return "" } +func (g *goGenerator) goErrorResult(call CallExpr, rendered, expectedType string) (string, bool) { + base, args, ok := parseGenericType(expectedType) + if !ok || base != "Result" || len(args) != 2 || args[1] != "Error" || !g.isExternalGoCall(call.Callee) { + return "", false + } + if actualBase, _, actualIsResult := parseGenericType(g.exprType(call)); actualIsResult && actualBase == "Result" { + return "", false + } + valueType := mapGoType(args[0]) + if args[0] == "Unit" { + valueType = "struct{}" + return fmt.Sprintf("func() GotlinResult[%s] { err := %s; return GotlinResult[%s]{Err: err} }()", valueType, rendered, valueType), true + } + return fmt.Sprintf("func() GotlinResult[%s] { value, err := %s; return GotlinResult[%s]{Value: value, Err: err} }()", valueType, rendered, valueType), true +} + +func (g *goGenerator) isExternalGoCall(callee Expr) bool { + switch value := callee.(type) { + case IdentExpr: + if _, ok := g.functions[value.Name]; ok { + return false + } + if _, ok := g.classes[value.Name]; ok { + return false + } + return true + case SelectorExpr: + if receiver, ok := value.Receiver.(IdentExpr); ok && g.imports[receiver.Name] { + return true + } + if _, ok := g.classForType(g.exprType(value.Receiver)); ok { + return false + } + if _, ok := g.workerForType(g.exprType(value.Receiver)); ok { + return false + } + return true + default: + return false + } +} + func (g *goGenerator) classForType(typ string) (ClassDecl, bool) { typ = strings.TrimPrefix(typ, "*") typ = strings.TrimSuffix(typ, "?") @@ -2690,8 +3221,26 @@ func (g *goGenerator) cloneTypeScopes() []map[string]string { } func renderGoParams(params []Param) string { + return renderGoParamsWithPrefix(params, "") +} + +func renderGoFunctionParams(function FunctionDecl) string { + prefix := "" + if function.Suspend { + prefix = "gotlinScope *GotlinCoroutineScope" + } + return renderGoParamsWithPrefix(function.Params, prefix) +} + +func renderGoParamsWithPrefix(params []Param, prefix string) string { var b strings.Builder b.WriteString("(") + if prefix != "" { + b.WriteString(prefix) + if len(params) > 0 { + b.WriteString(", ") + } + } for i, param := range params { if i > 0 { b.WriteString(", ") @@ -2904,6 +3453,10 @@ func usedImportAliases(program *Program) map[string]bool { used[e.Name] = true case UnaryExpr: walkExpr(e.Value) + case NonNullExpr: + walkExpr(e.Value) + case TryExpr: + walkExpr(e.Value) case BinaryExpr: walkExpr(e.Left) walkExpr(e.Right) @@ -2912,11 +3465,23 @@ func usedImportAliases(program *Program) map[string]bool { for _, arg := range e.Args { walkExpr(arg) } + for _, arg := range e.NamedArgs { + walkExpr(arg.Value) + } case SelectorExpr: if alias, ok := selectorRootAlias(e); ok { used[alias] = true } walkExpr(e.Receiver) + case SafeSelectorExpr: + walkExpr(e.Receiver) + case IndexExpr: + walkExpr(e.Receiver) + walkExpr(e.Index) + case EnumVariantExpr: + for _, value := range e.Values { + walkExpr(value) + } case LambdaExpr: for _, param := range e.Params { markType(param.Type) @@ -2947,6 +3512,8 @@ func usedImportAliases(program *Program) map[string]bool { walkExpr(s.Value) case GoStmt: walkExpr(s.Value) + case DeferStmt: + walkExpr(s.Value) case ExprStmt: walkExpr(s.Value) case IfStmt: @@ -2962,6 +3529,11 @@ func usedImportAliases(program *Program) map[string]bool { for _, inner := range s.Body { walkStmt(inner) } + case ForEachStmt: + walkExpr(s.Source) + for _, inner := range s.Body { + walkStmt(inner) + } case SelectStmt: for _, c := range s.Cases { walkExpr(c.Source) @@ -2969,6 +3541,13 @@ func usedImportAliases(program *Program) map[string]bool { walkStmt(inner) } } + case MatchStmt: + walkExpr(s.Value) + for _, c := range s.Cases { + for _, inner := range c.Body { + walkStmt(inner) + } + } case TryCatchStmt: for _, inner := range s.TryBody { walkStmt(inner) @@ -3025,6 +3604,9 @@ func usedImportAliases(program *Program) map[string]bool { walkStmt(stmt) } } + for _, embedded := range program.Embeds { + markType(embedded.Type) + } return used } @@ -3136,8 +3718,8 @@ func (g *goGenerator) emitRunCatchingSupport() { g.line("}") } -func (g *goGenerator) emitAutoThrowSupport() { - g.line("func gotlinAutoThrow[T any](value T, rest ...any) T {") +func (g *goGenerator) emitGoUnwrapSupport() { + g.line("func gotlinUnwrapGo[T any](value T, rest ...any) T {") g.indentLevel++ g.line("if len(rest) == 1 {") g.indentLevel++ @@ -3208,13 +3790,7 @@ func (g *goGenerator) emitEveryMsSupport() { g.line("}") } -func (g *goGenerator) autoThrowValue(original Expr, rendered string) string { - if _, _, _, sql := splitSQLChain(original); sql { - return rendered - } - if _, ok := original.(CallExpr); ok { - return "gotlinAutoThrow(" + rendered + ")" - } +func (g *goGenerator) passthroughValue(original Expr, rendered string) string { return rendered } diff --git a/internal/lang/json_decode_test.go b/internal/lang/json_decode_test.go index e51fa70..3d7b2a8 100644 --- a/internal/lang/json_decode_test.go +++ b/internal/lang/json_decode_test.go @@ -12,7 +12,7 @@ package demo import json encoding.json fun decode(body: ByteSlice): List { - val accounts = json.decode>(body) + val accounts = json.decode>(body).unwrap() return accounts } `) @@ -23,7 +23,7 @@ fun decode(body: ByteSlice): List { if err != nil { t.Fatalf("generation failed: %v", err) } - for _, want := range []string{"accounts := gotlinAutoThrow(gotlinJSONDecode[[]Account](body))", "json.Unmarshal(body, &value)"} { + for _, want := range []string{"accounts := gotlinResultUnwrap(gotlinJSONDecode[[]Account](body))", "json.Unmarshal(body, &value)"} { if !strings.Contains(string(out), want) { t.Fatalf("generated Go missing %q:\n%s", want, out) } diff --git a/internal/lang/lexer.go b/internal/lang/lexer.go index a2ca31a..99ce740 100644 --- a/internal/lang/lexer.go +++ b/internal/lang/lexer.go @@ -122,6 +122,9 @@ func (l *lexer) next() (token, error) { case '%': return token{kind: tokenPercent, lexeme: "%", pos: start}, nil case '!': + if l.match('!') { + return token{kind: tokenDoubleBang, lexeme: "!!", pos: start}, nil + } if l.match('=') { return token{kind: tokenNeq, lexeme: "!=", pos: start}, nil } @@ -149,6 +152,9 @@ func (l *lexer) next() (token, error) { case '@': return token{kind: tokenAt, lexeme: "@", pos: start}, nil case '?': + if l.match('.') { + return token{kind: tokenSafeDot, lexeme: "?.", pos: start}, nil + } return token{kind: tokenQuestion, lexeme: "?", pos: start}, nil case '|': if l.match('|') { diff --git a/internal/lang/nullability_test.go b/internal/lang/nullability_test.go new file mode 100644 index 0000000..607be98 --- /dev/null +++ b/internal/lang/nullability_test.go @@ -0,0 +1,67 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestSafeAccessAndNonNullAssertion(t *testing.T) { + prog, err := Parse(`package demo +data class User(var email: String) +fun safe(user: *User?): String? { return user?.email } +fun required(user: *User?): String { return user!!.email }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"if value == nil {", "return &result", "non-null assertion failed"} { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestRejectNullableDereference(t *testing.T) { + prog, err := Parse(`package demo +fun unsafe(user: *User?): String { return user.email }`) + if err != nil { + t.Fatal(err) + } + _, err = GenerateGo(prog) + if err == nil || !strings.Contains(err.Error(), "requires ?. or !!") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRejectNullForNonNullableTypes(t *testing.T) { + for _, source := range []string{ + `package demo data class User(var email: String) fun main() { val user: *User = null }`, + `package demo data class User(var email: String) fun use(user: *User) {} fun main() { use(null) }`, + `package demo fun name(): String { return null }`, + `package demo fun main() { val value = null }`, + } { + prog, err := Parse(source) + if err != nil { + t.Fatal(err) + } + if _, err = GenerateGo(prog); err == nil { + t.Fatalf("expected nullability error for %s", source) + } + } +} + +func TestNullableSmartCasts(t *testing.T) { + prog, err := Parse(`package demo +data class User(var email: String) +fun guarded(user: *User?): String { if (user == null) { return "missing" }; return user.email } +fun branched(user: *User?): String { if (user != null) { return user.email } else { return "missing" } }`) + if err != nil { + t.Fatal(err) + } + if _, err = GenerateGo(prog); err != nil { + t.Fatal(err) + } +} diff --git a/internal/lang/parser.go b/internal/lang/parser.go index 73e140a..0de8dad 100644 --- a/internal/lang/parser.go +++ b/internal/lang/parser.go @@ -137,7 +137,7 @@ func (p *parser) parseProgram() (*Program, error) { return nil, err } prog.Workers = append(prog.Workers, decl) - case p.check(tokenFun): + case p.check(tokenFun) || p.check(tokenSuspend): fn, err := p.parseFunction() if err != nil { return nil, err @@ -320,7 +320,7 @@ func (p *parser) parseClass() (ClassDecl, error) { var methods []FunctionDecl for !p.check(tokenRBrace) && !p.check(tokenEOF) { p.match(tokenOverride) - if !p.check(tokenFun) { + if !p.check(tokenFun) && !p.check(tokenSuspend) { tok := p.peek() return ClassDecl{}, fmt.Errorf("expected class member at %d, found %q", tok.pos, tok.lexeme) } @@ -538,10 +538,12 @@ func (p *parser) parseFunction() (FunctionDecl, error) { Params: signature.Params, ReturnType: signature.ReturnType, Body: body, + Suspend: signature.Suspend, }, nil } func (p *parser) parseFunctionSignature() (FunctionSignature, error) { + suspend := p.match(tokenSuspend) if _, err := p.expect(tokenFun, "expected 'fun'"); err != nil { return FunctionSignature{}, err } @@ -573,6 +575,7 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) { Name: name.lexeme, Params: params, ReturnType: returnType, + Suspend: suspend, }, nil } @@ -620,6 +623,9 @@ func (p *parser) parseBlock() ([]Stmt, error) { } func (p *parser) parseStmt() (Stmt, error) { + if p.check(tokenIdent) && p.peek().lexeme == "go" { + return nil, fmt.Errorf("bare go is removed; use launch inside a coroutine scope") + } switch { case p.match(tokenVal): return p.parseVarDecl(false) @@ -1105,6 +1111,16 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) { return nil, fmt.Errorf("expected selector name at %d, found %q", name.pos, name.lexeme) } expr = SelectorExpr{Receiver: expr, Name: name.lexeme} + case p.match(tokenSafeDot): + name := p.advance() + if !selectorName(name.lexeme) { + return nil, fmt.Errorf("expected selector name at %d, found %q", name.pos, name.lexeme) + } + expr = SafeSelectorExpr{Receiver: expr, Name: name.lexeme} + case p.match(tokenDoubleBang): + expr = NonNullExpr{Value: expr} + case p.match(tokenQuestion): + expr = TryExpr{Value: expr} case p.match(tokenLBracket): index, err := p.parseExpr(0) if err != nil { @@ -1122,6 +1138,10 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) { if !hasTypeArgs { return expr, nil } + if p.check(tokenLBrace) { + expr = CallExpr{Callee: expr, TypeArgs: typeArgs} + continue + } if _, err := p.expect(tokenLParen, "expected '(' after generic type arguments"); err != nil { return nil, err } @@ -1149,6 +1169,8 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) { call = current case SelectorExpr: call = CallExpr{Callee: current} + case IdentExpr: + call = CallExpr{Callee: current} default: return expr, nil } @@ -1236,7 +1258,7 @@ func (p *parser) tryParseCallTypeArgs() ([]string, bool, error) { p.pos = saved return nil, false, nil } - if !p.check(tokenLParen) { + if !p.check(tokenLParen) && !p.check(tokenLBrace) { p.pos = saved return nil, false, nil } diff --git a/internal/lang/references.go b/internal/lang/references.go new file mode 100644 index 0000000..9ef96be --- /dev/null +++ b/internal/lang/references.go @@ -0,0 +1,192 @@ +package lang + +import "strings" + +func normalizeClassReferences(program *Program) { + classes := map[string]bool{} + for _, class := range program.Classes { + classes[class.Name] = true + } + normalize := func(value string) string { return normalizeReferenceType(value, classes) } + for i := range program.Interfaces { + for j := range program.Interfaces[i].Methods { + normalizeSignature(&program.Interfaces[i].Methods[j], normalize) + } + } + for i := range program.Enums { + for j := range program.Enums[i].Variants { + for k := range program.Enums[i].Variants[j].PayloadTypes { + program.Enums[i].Variants[j].PayloadTypes[k] = normalize(program.Enums[i].Variants[j].PayloadTypes[k]) + } + } + } + for i := range program.Classes { + for j := range program.Classes[i].Fields { + program.Classes[i].Fields[j].Type = normalize(program.Classes[i].Fields[j].Type) + } + for j := range program.Classes[i].Methods { + normalizeFunction(&program.Classes[i].Methods[j], normalize) + } + } + for i := range program.Workers { + for j := range program.Workers[i].Fields { + program.Workers[i].Fields[j].Type = normalize(program.Workers[i].Fields[j].Type) + normalizeExprTypes(program.Workers[i].Fields[j].Value, normalize) + } + for j := range program.Workers[i].Methods { + normalizeFunction(&program.Workers[i].Methods[j], normalize) + } + } + for i := range program.Functions { + normalizeFunction(&program.Functions[i], normalize) + } +} + +func normalizeSignature(signature *FunctionSignature, normalize func(string) string) { + for i := range signature.Params { + signature.Params[i].Type = normalize(signature.Params[i].Type) + } + signature.ReturnType = normalize(signature.ReturnType) +} + +func normalizeFunction(function *FunctionDecl, normalize func(string) string) { + for i := range function.Params { + function.Params[i].Type = normalize(function.Params[i].Type) + } + function.ReturnType = normalize(function.ReturnType) + normalizeStmtTypes(function.Body, normalize) +} + +func normalizeStmtTypes(statements []Stmt, normalize func(string) string) { + for index, statement := range statements { + switch value := statement.(type) { + case VarDecl: + value.Type = normalize(value.Type) + normalizeExprTypes(value.Value, normalize) + statements[index] = value + case MultiVarDecl: + normalizeExprTypes(value.Value, normalize) + case AssignStmt: + normalizeExprTypes(value.Value, normalize) + case AddAssignStmt: + normalizeExprTypes(value.Value, normalize) + case MultiAssignStmt: + normalizeExprTypes(value.Value, normalize) + case ReturnStmt: + if value.Value != nil { + normalizeExprTypes(value.Value, normalize) + } + case ThrowStmt: + normalizeExprTypes(value.Value, normalize) + case GoStmt: + normalizeExprTypes(value.Value, normalize) + case DeferStmt: + normalizeExprTypes(value.Value, normalize) + case ExprStmt: + normalizeExprTypes(value.Value, normalize) + case IfStmt: + normalizeExprTypes(value.Cond, normalize) + normalizeStmtTypes(value.Then, normalize) + normalizeStmtTypes(value.Else, normalize) + case WhileStmt: + normalizeExprTypes(value.Cond, normalize) + normalizeStmtTypes(value.Body, normalize) + case ForEachStmt: + normalizeExprTypes(value.Source, normalize) + normalizeStmtTypes(value.Body, normalize) + case SelectStmt: + for _, c := range value.Cases { + normalizeExprTypes(c.Source, normalize) + normalizeStmtTypes(c.Body, normalize) + } + case MatchStmt: + normalizeExprTypes(value.Value, normalize) + for _, c := range value.Cases { + normalizeStmtTypes(c.Body, normalize) + } + case TryCatchStmt: + value.CatchType = normalize(value.CatchType) + normalizeStmtTypes(value.TryBody, normalize) + normalizeStmtTypes(value.CatchBody, normalize) + statements[index] = value + } + } +} + +func normalizeExprTypes(expression Expr, normalize func(string) string) { + switch value := expression.(type) { + case UnaryExpr: + normalizeExprTypes(value.Value, normalize) + case NonNullExpr: + normalizeExprTypes(value.Value, normalize) + case BinaryExpr: + normalizeExprTypes(value.Left, normalize) + normalizeExprTypes(value.Right, normalize) + case SelectorExpr: + normalizeExprTypes(value.Receiver, normalize) + case SafeSelectorExpr: + normalizeExprTypes(value.Receiver, normalize) + case IndexExpr: + normalizeExprTypes(value.Receiver, normalize) + normalizeExprTypes(value.Index, normalize) + case EnumVariantExpr: + for _, item := range value.Values { + normalizeExprTypes(item, normalize) + } + case LambdaExpr: + for i := range value.Params { + value.Params[i].Type = normalize(value.Params[i].Type) + } + normalizeStmtTypes(value.Body, normalize) + case CallExpr: + skipTypeArgs := false + if selector, ok := value.Callee.(SelectorExpr); ok { + if root, ok := selector.Receiver.(IdentExpr); ok && root.Name == "sql" { + skipTypeArgs = true + } + } + if !skipTypeArgs { + for i := range value.TypeArgs { + value.TypeArgs[i] = normalize(value.TypeArgs[i]) + } + } + normalizeExprTypes(value.Callee, normalize) + for _, item := range value.Args { + normalizeExprTypes(item, normalize) + } + for _, item := range value.NamedArgs { + normalizeExprTypes(item.Value, normalize) + } + } +} + +func normalizeReferenceType(value string, classes map[string]bool) string { + if value == "" { + return value + } + nullable := strings.HasSuffix(value, "?") + if nullable { + value = strings.TrimSuffix(value, "?") + } + explicitPointer := strings.HasPrefix(value, "*") + if explicitPointer { + value = strings.TrimPrefix(value, "*") + } + if params, result, ok := parseFunctionType(value); ok { + for i := range params { + params[i] = normalizeReferenceType(params[i], classes) + } + value = "(" + strings.Join(params, ", ") + ") -> " + normalizeReferenceType(result, classes) + } else if base, args, ok := parseGenericType(value); ok { + for i := range args { + args[i] = normalizeReferenceType(args[i], classes) + } + value = base + "<" + strings.Join(args, ", ") + ">" + } else if classes[value] || explicitPointer { + value = "*" + value + } + if nullable { + value += "?" + } + return value +} diff --git a/internal/lang/references_test.go b/internal/lang/references_test.go new file mode 100644 index 0000000..3512718 --- /dev/null +++ b/internal/lang/references_test.go @@ -0,0 +1,43 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestGotlinClassesAreReferenceTypesByDefault(t *testing.T) { + prog, err := Parse(`package demo +class Repository +class Service(val repository: Repository) +fun create(): Service { return Service(Repository()) } +fun many(): List { return listOf(Repository()) } +fun optional(): Repository? { return null }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"repository *Repository", "func create() *Service", "func many() []*Repository", "func optional() *Repository"} { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestExplicitClassPointerRemainsCompatible(t *testing.T) { + prog, err := Parse(`package demo +class Repository +fun use(repository: *Repository): *Repository { return repository }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(out), "**Repository") || !strings.Contains(string(out), "repository *Repository") { + t.Fatalf("unexpected explicit pointer output:\n%s", out) + } +} diff --git a/internal/lang/result_test.go b/internal/lang/result_test.go new file mode 100644 index 0000000..1965560 --- /dev/null +++ b/internal/lang/result_test.go @@ -0,0 +1,142 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestResultQuestionPropagatesGoError(t *testing.T) { + prog, err := Parse(`package demo +import strconv +fun parse(value: String): Result { + val parsed = strconv.atoi(value)? + return Result::Ok(parsed) +}`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"parsed, gotlinError1 := strconv.Atoi(value)", "if gotlinError1 != nil {", "GotlinResult[int]{Err: gotlinError1}", "GotlinResult[int]{Value: parsed}"} { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestGoValueAndErrorReturnConvertsToResult(t *testing.T) { + prog, err := Parse(`package demo +import strconv +fun parse(value: String): Result { return strconv.atoi(value) } +fun assign(value: String) { val result: Result = strconv.atoi(value); println(result.unwrapOr(0)) }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "value, err := strconv.Atoi(value)", + "GotlinResult[int]{Value: value, Err: err}", + "var result GotlinResult[int] = func() GotlinResult[int]", + } { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestGoErrorOnlyReturnConvertsToUnitResult(t *testing.T) { + prog, err := Parse(`package demo +import os +fun changeDirectory(path: String): Result { return os.chdir(path) }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "func changeDirectory(path string) GotlinResult[struct{}]", + "err := os.Chdir(path)", + "GotlinResult[struct{}]{Err: err}", + } { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestResultQuestionPropagatesGotlinResult(t *testing.T) { + prog, err := Parse(`package demo +import errors +fun inner(ok: Boolean): Result { if (!ok) { return Result::Err(errors.new("failed")) }; return Result::Ok("ok") } +fun outer(): Result { val value = inner(true)?; return Result::Ok(value) }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"gotlinResult1 := inner(true)", "gotlinResult1.Err", "value := gotlinResult1.Value"} { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestResultUnwrapAndUnwrapOr(t *testing.T) { + prog, err := Parse(`package demo +fun value(result: Result): String { return result.unwrapOr("fallback") } +fun required(result: Result): String { return result.unwrap() }`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"gotlinResultUnwrapOr(result, \"fallback\")", "gotlinResultUnwrap(result)"} { + if !strings.Contains(string(out), want) { + t.Fatalf("missing %q:\n%s", want, out) + } + } +} + +func TestQuestionRequiresResultFunction(t *testing.T) { + prog, err := Parse(`package demo +import strconv +fun parse(value: String): Int { val parsed = strconv.atoi(value)?; return parsed }`) + if err != nil { + t.Fatal(err) + } + _, err = GenerateGo(prog) + if err == nil || !strings.Contains(err.Error(), "returning Result") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestQuestionUseKeepsReferencedVariableLive(t *testing.T) { + prog, err := Parse(`package demo +import strconv +fun parse(value: String): Result { + val input = value + val parsed = strconv.atoi(input)? + return Result::Ok(parsed) +}`) + if err != nil { + t.Fatal(err) + } + out, err := GenerateGo(prog) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(out), "input := value") || strings.Contains(string(out), "_ = value") { + t.Fatalf("variable used by ? expression was removed:\n%s", out) + } +} diff --git a/internal/lang/semantics.go b/internal/lang/semantics.go index c6abac2..16df2df 100644 --- a/internal/lang/semantics.go +++ b/internal/lang/semantics.go @@ -208,6 +208,12 @@ func (c *mutabilityChecker) checkExpr(expr Expr) error { } case SelectorExpr: return c.checkExpr(e.Receiver) + case SafeSelectorExpr: + return c.checkExpr(e.Receiver) + case NonNullExpr: + return c.checkExpr(e.Value) + case TryExpr: + return c.checkExpr(e.Value) case IndexExpr: if err := c.checkExpr(e.Receiver); err != nil { return err diff --git a/internal/lang/sql.go b/internal/lang/sql.go index 2c8f909..ca25899 100644 --- a/internal/lang/sql.go +++ b/internal/lang/sql.go @@ -47,6 +47,11 @@ func splitSQLChain(expr Expr) (CallExpr, string, []sqlCallStep, bool) { } func (g *goGenerator) lowerSQLQuery(expr Expr) (string, bool, error) { + if call, ok := expr.(CallExpr); ok { + if selector, ok := call.Callee.(SelectorExpr); ok && (selector.Name == "unwrap" || selector.Name == "unwrapOr") { + return "", false, nil + } + } root, operation, steps, ok := splitSQLChain(expr) if !ok { return "", false, nil @@ -166,11 +171,11 @@ func sqlChainResultType(expr Expr) (string, bool) { } switch terminal { case "fetch": - return "List<*" + resultType + ">", true + return "Result, Error>", true case "single": - return "*" + resultType, true + return "Result<*" + resultType + ", Error>", true case "iterator": - return "GotlinSQLIterator<" + resultType + ">", true + return "Result, Error>", true default: return "", false } diff --git a/internal/lang/sql_expansion_test.go b/internal/lang/sql_expansion_test.go index 19be4e6..cf1f72a 100644 --- a/internal/lang/sql_expansion_test.go +++ b/internal/lang/sql_expansion_test.go @@ -68,18 +68,18 @@ fun events(pool: *pgxpool.Pool, ctx: context.Context): List<*EventProjection> { return sql.from() .select { row -> EventProjection(row.id, row.payload) } .orderBy { it.createdAt } - .fetch(pool, ctx) + .fetch(pool, ctx).unwrap() } fun eventStream(pool: *pgxpool.Pool, ctx: context.Context): GotlinSQLIterator { return sql.from() .select { row -> EventProjection(row.id, row.payload) } - .iterator(pool, ctx) + .iterator(pool, ctx).unwrap() } `) for _, want := range []string{ - `return gotlinSQLFetch[EventProjection](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, payload FROM outbox_events ORDER BY created_at", Args: []any{}}, gotlinSQLScanEventProjection)`, - `return gotlinSQLIterate[EventProjection](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, payload FROM outbox_events", Args: []any{}}, gotlinSQLScanEventProjection)`, + `return gotlinResultUnwrap(gotlinSQLFetch[EventProjection](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, payload FROM outbox_events ORDER BY created_at", Args: []any{}}, gotlinSQLScanEventProjection))`, + `return gotlinResultUnwrap(gotlinSQLIterate[EventProjection](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, payload FROM outbox_events", Args: []any{}}, gotlinSQLScanEventProjection))`, `func gotlinSQLScanEventProjection(row gotlinSQLRow) (*EventProjection, error)`, `err := row.Scan(&value.Id, &value.Payload)`, } { @@ -97,7 +97,7 @@ import pgxpool "github.com/jackc/pgx/v5/pgxpool" fun create(row: EventRow, pool: *pgxpool.Pool, ctx: context.Context): *EventProjection { return sql.insert(row) .returning { value -> EventProjection(value.id, value.payload) } - .single(pool, ctx) + .single(pool, ctx).unwrap() } `) for _, want := range []string{ @@ -135,7 +135,7 @@ fun claim(id: String, payload: String, pool: *pgxpool.Pool, ctx: context.Context } .where { it.id == id && it.publishedAt == null } .returning { row -> EventProjection(row.id, row.payload) } - .single(pool, ctx) + .single(pool, ctx).unwrap() } `) for _, want := range []string{ @@ -158,7 +158,7 @@ fun remove(id: String, pool: *pgxpool.Pool, ctx: context.Context): *EventRow { return sql.delete() .where { it.id == id } .returning { it } - .single(pool, ctx) + .single(pool, ctx).unwrap() } `) for _, want := range []string{ @@ -245,7 +245,7 @@ func TestRejectExpandedInvalidSQLQueries(t *testing.T) { }, { name: "write execution without returning", - src: eventSQLSource + `fun query(pool: Any, ctx: Any): *EventRow { return sql.delete().single(pool, ctx) }`, + src: eventSQLSource + `fun query(pool: Any, ctx: Any): *EventRow { return sql.delete().single(pool, ctx).unwrap() }`, want: "requires returning()", }, { diff --git a/internal/lang/sql_test.go b/internal/lang/sql_test.go index dddcd25..cedb35a 100644 --- a/internal/lang/sql_test.go +++ b/internal/lang/sql_test.go @@ -97,15 +97,15 @@ import pgxpool "github.com/jackc/pgx/v5/pgxpool" fun accounts(pool: *pgxpool.Pool, ctx: context.Context, customerId: String): List<*AccountRow> { return sql.from() .where { it.customerId == customerId } - .fetch(pool, ctx) + .fetch(pool, ctx).unwrap() } `) for _, want := range []string{ `"github.com/jackc/pgx/v5"`, `func accounts(pool *pgxpool.Pool, ctx context.Context, customerId string) []*AccountRow`, - `return gotlinSQLFetch[AccountRow](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, customer_id, kind, balance FROM accounts WHERE customer_id = $1", Args: []any{customerId}}, gotlinSQLScanAccountRow)`, + `return gotlinResultUnwrap(gotlinSQLFetch[AccountRow](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, customer_id, kind, balance FROM accounts WHERE customer_id = $1", Args: []any{customerId}}, gotlinSQLScanAccountRow))`, `defer rows.Close()`, - `values = append(values, gotlinAutoThrow(scan(rows)))`, + `values = append(values, value)`, `err := row.Scan(&value.Id, &value.CustomerId, &value.AccountType, &value.Balance)`, } { if !strings.Contains(code, want) { @@ -120,15 +120,15 @@ import context import pgxpool "github.com/jackc/pgx/v5/pgxpool" fun balance(pool: *pgxpool.Pool, ctx: context.Context, id: String): Double { - val account = sql.from().where { it.id == id }.single(pool, ctx) + val account = sql.from().where { it.id == id }.single(pool, ctx).unwrap() return account.balance } `) for _, want := range []string{ - `account := gotlinSQLSingle[AccountRow](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, customer_id, kind, balance FROM accounts WHERE id = $1", Args: []any{id}}, gotlinSQLScanAccountRow)`, + `account := gotlinResultUnwrap(gotlinSQLSingle[AccountRow](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, customer_id, kind, balance FROM accounts WHERE id = $1", Args: []any{id}}, gotlinSQLScanAccountRow))`, `return account.Balance`, - `gotlinAutoThrow(gotlinSQLError("SQL single() expected exactly one row, got zero"))`, - `gotlinAutoThrow(gotlinSQLError("SQL single() expected exactly one row, got more than one"))`, + `GotlinResult[*T]{Err: gotlinSQLError("SQL single() expected exactly one row, got zero")}`, + `GotlinResult[*T]{Err: gotlinSQLError("SQL single() expected exactly one row, got more than one")}`, } { if !strings.Contains(code, want) { t.Fatalf("generated Go missing %q:\n%s", want, code) @@ -142,7 +142,7 @@ import context import pgxpool "github.com/jackc/pgx/v5/pgxpool" fun printAccounts(pool: *pgxpool.Pool, ctx: context.Context) { - val rows = sql.from().iterator(pool, ctx) + val rows = sql.from().iterator(pool, ctx).unwrap() defer rows.close() while (rows.next()) { val account = rows.value() @@ -152,12 +152,12 @@ fun printAccounts(pool: *pgxpool.Pool, ctx: context.Context) { } `) for _, want := range []string{ - `rows := gotlinSQLIterate[AccountRow](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, customer_id, kind, balance FROM accounts", Args: []any{}}, gotlinSQLScanAccountRow)`, + `rows := gotlinResultUnwrap(gotlinSQLIterate[AccountRow](pool, ctx, GotlinSQLQuery{SQL: "SELECT id, customer_id, kind, balance FROM accounts", Args: []any{}}, gotlinSQLScanAccountRow))`, `defer rows.close()`, `for rows.next()`, - `account := gotlinAutoThrow(rows.value())`, + `account := rows.value()`, `fmt.Println(account.CustomerId)`, - `_ = gotlinAutoThrow(rows.err())`, + `_ = rows.err()`, `type GotlinSQLIterator[T any] struct`, `func (iterator *GotlinSQLIterator[T]) next() bool`, `func (iterator *GotlinSQLIterator[T]) value() *T`, @@ -304,17 +304,17 @@ fun query(row: OtherRow): GotlinSQLQuery { return sql.insert(row).on }, { name: "fetch missing arguments", - src: accountRowSource + `fun query(): List<*AccountRow> { return sql.from().fetch() }`, + src: accountRowSource + `fun query(): List<*AccountRow> { return sql.from().fetch().unwrap() }`, want: "fetch() expects exactly pool and ctx positional arguments", }, { name: "single extra argument", - src: accountRowSource + `fun query(pool: Any, ctx: Any): *AccountRow { return sql.from().single(pool, ctx, ctx) }`, + src: accountRowSource + `fun query(pool: Any, ctx: Any): *AccountRow { return sql.from().single(pool, ctx, ctx).unwrap() }`, want: "single() expects exactly pool and ctx positional arguments", }, { name: "iterator named arguments", - src: accountRowSource + `fun query(pool: Any, ctx: Any): GotlinSQLIterator { return sql.from().iterator(pool = pool, ctx = ctx) }`, + src: accountRowSource + `fun query(pool: Any, ctx: Any): GotlinSQLIterator { return sql.from().iterator(pool = pool, ctx = ctx).unwrap() }`, want: "iterator() expects exactly pool and ctx positional arguments", }, { @@ -324,17 +324,17 @@ fun query(row: OtherRow): GotlinSQLQuery { return sql.insert(row).on }, { name: "fetch invalid pool type", - src: accountRowSource + `fun query(pool: String, ctx: Any): List<*AccountRow> { return sql.from().fetch(pool, ctx) }`, + src: accountRowSource + `fun query(pool: String, ctx: Any): List<*AccountRow> { return sql.from().fetch(pool, ctx).unwrap() }`, want: "pool argument has non-query type String", }, { name: "single invalid context type", - src: accountRowSource + `fun query(pool: Any, ctx: Int): *AccountRow { return sql.from().single(pool, ctx) }`, + src: accountRowSource + `fun query(pool: Any, ctx: Int): *AccountRow { return sql.from().single(pool, ctx).unwrap() }`, want: "ctx argument has non-context type Int", }, { name: "insert execution terminal", - src: accountRowSource + `fun query(row: AccountRow, pool: Any, ctx: Any): List<*AccountRow> { return sql.insert(row).fetch(pool, ctx) }`, + src: accountRowSource + `fun query(row: AccountRow, pool: Any, ctx: Any): List<*AccountRow> { return sql.insert(row).fetch(pool, ctx).unwrap() }`, want: "fetch() is only supported for sql.from", }, } diff --git a/internal/lang/token.go b/internal/lang/token.go index a3b41e5..5dbded9 100644 --- a/internal/lang/token.go +++ b/internal/lang/token.go @@ -20,6 +20,7 @@ const ( tokenEnum tokenKind = "ENUM" tokenMatch tokenKind = "MATCH" tokenFun tokenKind = "FUN" + tokenSuspend tokenKind = "SUSPEND" tokenOverride tokenKind = "OVERRIDE" tokenPrivate tokenKind = "PRIVATE" tokenVal tokenKind = "VAL" @@ -65,17 +66,19 @@ const ( tokenAmp tokenKind = "&" tokenAt tokenKind = "@" tokenQuestion tokenKind = "?" + tokenSafeDot tokenKind = "?." + tokenDoubleBang tokenKind = "!!" tokenOr tokenKind = "||" tokenArrow tokenKind = "->" ) var keywords = map[string]tokenKind{ "fun": tokenFun, + "suspend": tokenSuspend, "import": tokenImport, "package": tokenPackage, "class": tokenClass, "data": tokenData, - "worker": tokenWorker, "interface": tokenInterface, "enum": tokenEnum, "match": tokenMatch, @@ -90,7 +93,6 @@ var keywords = map[string]tokenKind{ "in": tokenIn, "select": tokenSelect, "return": tokenReturn, - "go": tokenGo, "defer": tokenDefer, "try": tokenTry, "catch": tokenCatch, diff --git a/tools/vscode-gotlin/README.md b/tools/vscode-gotlin/README.md index 9cb014f..b33839a 100644 --- a/tools/vscode-gotlin/README.md +++ b/tools/vscode-gotlin/README.md @@ -8,7 +8,7 @@ VS Code language support for Gotlin (`.gt`) files. - Dedicated highlighting for the typed `sql` DSL and its query, mutation, and execution methods - Go import highlighting for bare dotted paths, aliases, and quoted module paths - Bracket matching, indentation, folding markers, comments, and editor pairs -- Snippets for data classes, SQL table rows and operations, workers, embeds, concurrency, defer, and foreach loops +- Snippets for data classes, SQL table rows and operations, structured coroutines, embeds, defer, and foreach loops - `gotlin-lsp` integration, including its optional `gopls` bridge for Go-imported symbols ## Development diff --git a/tools/vscode-gotlin/scripts/validate.js b/tools/vscode-gotlin/scripts/validate.js index cb558eb..622b708 100644 --- a/tools/vscode-gotlin/scripts/validate.js +++ b/tools/vscode-gotlin/scripts/validate.js @@ -42,12 +42,13 @@ assert(language.folding?.markers?.start && language.indentationRules?.increaseIn const grammarSource = JSON.stringify(grammar); const expectedTokens = [ - "data", "class", "worker", "private", "override", "val", "var", "if", "else", - "while", "for", "in", "select", "return", "go", "defer", "try", "catch", + "data", "class", "suspend", "private", "override", "val", "var", "if", "else", + "while", "for", "in", "select", "return", "defer", "try", "catch", "throw", "enum", "match", "package", "import", "jsonNaming", "embed", "table", "column", "id", - "generated", "Int", "String", "Boolean", "Unit", "Double", "Float", "Any", - "ByteSlice", "List", "MutableList", "Map", "MutableMap", "Channel", "GotlinSQLQuery", + "generated", "Int", "Long", "String", "Boolean", "Unit", "Double", "Float", "Any", + "ByteSlice", "Error", "Result", "List", "MutableList", "Map", "MutableMap", "Channel", "GotlinSQLQuery", "GotlinSQLIterator", "from", "where", "orderBy", "orderByDescending", "limit", + "runBlocking", "coroutineScope", "launch", "async", "await", "delay", "withTimeout", "isActive", "forUpdate", "skipLocked", "insert", "update", "delete", "onConflict", "doNothing", "doUpdate", "returning", "build", "fetch", "single", "iterator", "set", "now", "mapTo" ]; @@ -81,7 +82,7 @@ for (const declaration of [ const prefixes = new Set(Object.values(snippets).map((snippet) => snippet.prefix)); for (const prefix of [ - "dataclass", "tablerow", "embed", "worker", "enum", "match", "mapto", "go", "defer", "foreach", "sqlfetch", + "dataclass", "tablerow", "embed", "coroutinescope", "launch", "async", "enum", "match", "mapto", "safe", "nonnull", "resultfun", "resultmatch", "defer", "foreach", "sqlfetch", "sqlsingle", "sqliterator", "sqlinsertnothing", "sqlinsertupdate", "sqlupdatereturning" ]) { assert(prefixes.has(prefix), `snippets are missing prefix ${prefix}`); diff --git a/tools/vscode-gotlin/snippets/gotlin.code-snippets b/tools/vscode-gotlin/snippets/gotlin.code-snippets index 5ce89d8..a6aa77e 100644 --- a/tools/vscode-gotlin/snippets/gotlin.code-snippets +++ b/tools/vscode-gotlin/snippets/gotlin.code-snippets @@ -52,19 +52,6 @@ "body": ["@embed(\"${1:path/to/file}\") val ${2:name}: ${3:ByteSlice}"], "description": "Embed a file as a top-level value" }, - "Worker": { - "prefix": "worker", - "body": [ - "worker ${1:Name} {", - " var ${2:state}: ${3:Int} = ${4:0}", - "", - " fun ${5:run}() {", - " $0", - " }", - "}" - ], - "description": "Stateful Gotlin worker" - }, "Rust-style Enum": { "prefix": "enum", "body": [ @@ -98,14 +85,50 @@ "body": ["val ${1:target} = ${2:source}.mapTo<${3:Target}>()"], "description": "Recursively map compatible classes or enums" }, - "Go Block": { - "prefix": "go", + "Nullable Safe Access": { + "prefix": "safe", + "body": ["${1:value}?.${2:field}"], + "description": "Safely access a nullable value" + }, + "Non-null Assertion": { + "prefix": "nonnull", + "body": ["${1:value}!!"], + "description": "Assert that a nullable value is non-null" + }, + "Result Function": { + "prefix": "resultfun", "body": [ - "go {", - " $0", + "fun ${1:name}(${2}): Result<${3:Value}, Error> {", + " val ${4:value} = ${5:operation}()?", + " return Result::Ok(${4:value})", "}" ], - "description": "Run a block concurrently" + "description": "Function with Rust-style Result propagation" + }, + "Result Match": { + "prefix": "resultmatch", + "body": [ + "match (${1:result}) {", + " Result::Ok(${2:value}) -> { $3 }", + " Result::Err(${4:error}) -> { $0 }", + "}" + ], + "description": "Match a Result value" + }, + "Coroutine Scope": { + "prefix": "coroutinescope", + "body": ["coroutineScope {", " $0", "}"], + "description": "Structured child coroutine scope" + }, + "Launch Coroutine": { + "prefix": "launch", + "body": ["launch {", " $0", "}"], + "description": "Launch a structured child coroutine" + }, + "Async Coroutine": { + "prefix": "async", + "body": ["val ${1:result} = async<${2:Type}> {", " $0", "}"], + "description": "Start a typed deferred coroutine" }, "Defer Call": { "prefix": "defer", @@ -124,7 +147,7 @@ "Typed SQL Fetch": { "prefix": "sqlfetch", "body": [ - "val ${1:rows}: List<*${2:Row}> = sql.from<${2:Row}>()", + "val ${1:rows}: List<${2:Row}> = sql.from<${2:Row}>()", " .where { ${3:it.id == id} }", " .fetch(${4:pool}, ${5:ctx})" ], @@ -133,7 +156,7 @@ "Typed SQL Single": { "prefix": "sqlsingle", "body": [ - "val ${1:row}: *${2:Row} = sql.from<${2:Row}>()", + "val ${1:row}: ${2:Row} = sql.from<${2:Row}>()", " .where { ${3:it.id == id} }", " .single(${4:pool}, ${5:ctx})" ], diff --git a/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json b/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json index 6bbb4f0..afd07fc 100644 --- a/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json +++ b/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json @@ -142,14 +142,6 @@ "2": { "name": "entity.name.type.interface.gotlin" } } }, - { - "name": "meta.worker.gotlin", - "match": "\\b(worker)\\s+([A-Za-z_][A-Za-z0-9_]*)", - "captures": { - "1": { "name": "storage.type.worker.gotlin" }, - "2": { "name": "entity.name.type.worker.gotlin" } - } - }, { "name": "meta.enum.gotlin", "match": "\\b(enum)\\s+([A-Za-z_][A-Za-z0-9_]*)", @@ -262,7 +254,7 @@ "patterns": [ { "name": "keyword.control.declaration.gotlin", - "match": "\\b(package|import|data|class|interface|worker|enum|fun)\\b" + "match": "\\b(package|import|data|class|interface|enum|suspend|fun)\\b" }, { "name": "storage.modifier.gotlin", @@ -282,7 +274,7 @@ }, { "name": "keyword.control.concurrency.gotlin", - "match": "\\b(select|go|defer)\\b" + "match": "\\b(select|defer|runBlocking|coroutineScope|launch|async|await|delay|withTimeout|isActive)\\b" }, { "name": "keyword.control.exception.gotlin", @@ -298,7 +290,7 @@ "patterns": [ { "name": "support.type.builtin.gotlin", - "match": "\\b(Int|String|Boolean|Unit|Double|Float|Any|ByteSlice|List|MutableList|Map|MutableMap|Channel|GotlinSQLQuery|GotlinSQLIterator)\\b" + "match": "\\b(Int|Long|String|Boolean|Unit|Double|Float|Any|Error|Result|ByteSlice|List|MutableList|Map|MutableMap|Channel|GotlinSQLQuery|GotlinSQLIterator)\\b" }, { "name": "support.type.qualified.gotlin", @@ -324,6 +316,14 @@ }, "typeOperators": { "patterns": [ + { + "name": "keyword.operator.nullsafe.gotlin", + "match": "\\?\\.|!!" + }, + { + "name": "keyword.operator.error-propagation.gotlin", + "match": "(?<=[a-z0-9_)\\]])\\?(?!\\.)" + }, { "name": "keyword.operator.type.nullable.gotlin", "match": "(?<=[A-Za-z0-9_>])\\?"