Add structured coroutines and explicit error handling
This commit is contained in:
parent
de1262b4cf
commit
fe62e81152
31 changed files with 1701 additions and 325 deletions
113
README.md
113
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<T, Error>` flows.
|
||||
An explicit `Result` return or variable type converts the Go return directly:
|
||||
|
||||
```kotlin
|
||||
fun parse(value: String): Result<Int, Error> {
|
||||
return strconv.atoi(value)
|
||||
}
|
||||
|
||||
fun ping(db: *sql.DB): Result<Unit, Error> {
|
||||
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<User>`. 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<Int> { 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<AccountSummary> {
|
||||
return sql.from<AccountRow>()
|
||||
.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<List<AccountRow>, 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<AccountRow> {
|
||||
return sql.from<AccountRow>()
|
||||
.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<AccountRow, Error>` 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<AccountRow>()
|
||||
.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<AccountRow>().iterator(pool, ctx)
|
||||
val rows = sql.from<AccountRow>().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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -2201,8 +2205,10 @@ var builtinDetails = map[string]string{
|
|||
"mutableListOf": "fun mutableListOf<T>(values: T...): MutableList<T>",
|
||||
"mapOf": "fun mapOf<K, V>(pairs: Any...): Map<K, V>",
|
||||
"mutableMapOf": "fun mutableMapOf<K, V>(pairs: Any...): MutableMap<K, V>",
|
||||
"ByteSlice": "fun ByteSlice(value: String): ByteSlice",
|
||||
"ByteSlice": "fun ByteSlice(value: String | Int): ByteSlice",
|
||||
"append": "fun append<T>(values: List<T>, value: T): List<T>",
|
||||
"keys": "fun keys<K, V>(values: Map<K, V>): List<K>",
|
||||
"goAssert": "fun goAssert<T>(value: Any): T",
|
||||
"len": "fun len(value: Any): Int",
|
||||
"cap": "fun cap(value: Any): Int",
|
||||
"make": "fun make<T>(size: Int): T",
|
||||
|
|
@ -2219,6 +2225,13 @@ var builtinDetails = map[string]string{
|
|||
"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<T>(block: suspend () -> T): Deferred<T>",
|
||||
"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 {
|
||||
|
|
|
|||
|
|
@ -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,10 +717,12 @@ fun writer(ch: Channel<Int>) {
|
|||
|
||||
fun main() {
|
||||
val ch = Channel<Int>()
|
||||
go writer(ch)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ enum ResponseState {
|
|||
|
||||
data class AccountEntity(
|
||||
var id: String,
|
||||
var address: *AddressEntity,
|
||||
var address: AddressEntity,
|
||||
var labels: List<String>,
|
||||
var state: EntityState
|
||||
)
|
||||
|
||||
data class AccountResponse(
|
||||
var address: *AddressResponse,
|
||||
var address: AddressResponse,
|
||||
var id: String,
|
||||
var labels: List<String>,
|
||||
var state: ResponseState
|
||||
|
|
|
|||
18
examples/nullability.gt
Normal file
18
examples/nullability.gt
Normal file
|
|
@ -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)
|
||||
}
|
||||
18
examples/results.gt
Normal file
18
examples/results.gt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import strconv
|
||||
import os
|
||||
|
||||
fun parse(value: String): Result<Int, Error> {
|
||||
return strconv.atoi(value)
|
||||
}
|
||||
|
||||
fun changeDirectory(path: String): Result<Unit, Error> {
|
||||
return os.chdir(path)
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(parse("42").unwrap())
|
||||
println(parse("invalid").unwrapOr(0))
|
||||
changeDirectory(".").unwrap()
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
||||
runBlocking {
|
||||
val ready = Channel<String>()
|
||||
go {
|
||||
select {
|
||||
after(120) -> ready.send("timer fired")
|
||||
}
|
||||
launch {
|
||||
delay(120)
|
||||
ready.send("timer fired")
|
||||
}
|
||||
select {
|
||||
ready -> println("channel says: " + it)
|
||||
}
|
||||
|
||||
select {
|
||||
every(50) -> println("one periodic tick")
|
||||
val answer = async<Int> {
|
||||
delay(50)
|
||||
return 42
|
||||
}
|
||||
fmt.Println("async value:", answer.await())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String, Int>): List<String> { 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,10 +1106,12 @@ fun writer(ch: Channel<Int>) {
|
|||
|
||||
fun main() {
|
||||
val ch = Channel<Int>()
|
||||
go writer(ch)
|
||||
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) {
|
||||
|
|
|
|||
124
internal/lang/coroutines.go
Normal file
124
internal/lang/coroutines.go
Normal file
|
|
@ -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
|
||||
}
|
||||
69
internal/lang/coroutines_test.go
Normal file
69
internal/lang/coroutines_test.go
Normal file
|
|
@ -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<Int> { 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Request>(body)
|
||||
val request = json.decode<Request>(body).unwrap()
|
||||
return request.email
|
||||
}
|
||||
`)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,7 @@ package demo
|
|||
import json encoding.json
|
||||
|
||||
fun decode(body: ByteSlice): List<Account> {
|
||||
val accounts = json.decode<List<Account>>(body)
|
||||
val accounts = json.decode<List<Account>>(body).unwrap()
|
||||
return accounts
|
||||
}
|
||||
`)
|
||||
|
|
@ -23,7 +23,7 @@ fun decode(body: ByteSlice): List<Account> {
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('|') {
|
||||
|
|
|
|||
67
internal/lang/nullability_test.go
Normal file
67
internal/lang/nullability_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
192
internal/lang/references.go
Normal file
192
internal/lang/references.go
Normal file
|
|
@ -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
|
||||
}
|
||||
43
internal/lang/references_test.go
Normal file
43
internal/lang/references_test.go
Normal file
|
|
@ -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<Repository> { 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)
|
||||
}
|
||||
}
|
||||
142
internal/lang/result_test.go
Normal file
142
internal/lang/result_test.go
Normal file
|
|
@ -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<Int, Error> {
|
||||
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<Int, Error> { return strconv.atoi(value) }
|
||||
fun assign(value: String) { val result: Result<Int, Error> = 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<Unit, Error> { 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<String, Error> { if (!ok) { return Result::Err(errors.new("failed")) }; return Result::Ok("ok") }
|
||||
fun outer(): Result<String, Error> { 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, Error>): String { return result.unwrapOr("fallback") }
|
||||
fun required(result: Result<String, Error>): 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<Int, Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<List<*" + resultType + ">, Error>", true
|
||||
case "single":
|
||||
return "*" + resultType, true
|
||||
return "Result<*" + resultType + ", Error>", true
|
||||
case "iterator":
|
||||
return "GotlinSQLIterator<" + resultType + ">", true
|
||||
return "Result<GotlinSQLIterator<" + resultType + ">, Error>", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,18 +68,18 @@ fun events(pool: *pgxpool.Pool, ctx: context.Context): List<*EventProjection> {
|
|||
return sql.from<EventRow>()
|
||||
.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<EventProjection> {
|
||||
return sql.from<EventRow>()
|
||||
.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<EventRow>(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<EventRow>()
|
||||
.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<EventRow>().single(pool, ctx) }`,
|
||||
src: eventSQLSource + `fun query(pool: Any, ctx: Any): *EventRow { return sql.delete<EventRow>().single(pool, ctx).unwrap() }`,
|
||||
want: "requires returning()",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<AccountRow>()
|
||||
.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<AccountRow>().where { it.id == id }.single(pool, ctx)
|
||||
val account = sql.from<AccountRow>().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<AccountRow>().iterator(pool, ctx)
|
||||
val rows = sql.from<AccountRow>().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<AccountRow>(row).on
|
|||
},
|
||||
{
|
||||
name: "fetch missing arguments",
|
||||
src: accountRowSource + `fun query(): List<*AccountRow> { return sql.from<AccountRow>().fetch() }`,
|
||||
src: accountRowSource + `fun query(): List<*AccountRow> { return sql.from<AccountRow>().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<AccountRow>().single(pool, ctx, ctx) }`,
|
||||
src: accountRowSource + `fun query(pool: Any, ctx: Any): *AccountRow { return sql.from<AccountRow>().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<AccountRow> { return sql.from<AccountRow>().iterator(pool = pool, ctx = ctx) }`,
|
||||
src: accountRowSource + `fun query(pool: Any, ctx: Any): GotlinSQLIterator<AccountRow> { return sql.from<AccountRow>().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<AccountRow>(row).on
|
|||
},
|
||||
{
|
||||
name: "fetch invalid pool type",
|
||||
src: accountRowSource + `fun query(pool: String, ctx: Any): List<*AccountRow> { return sql.from<AccountRow>().fetch(pool, ctx) }`,
|
||||
src: accountRowSource + `fun query(pool: String, ctx: Any): List<*AccountRow> { return sql.from<AccountRow>().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<AccountRow>().single(pool, ctx) }`,
|
||||
src: accountRowSource + `fun query(pool: Any, ctx: Int): *AccountRow { return sql.from<AccountRow>().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<AccountRow>(row).fetch(pool, ctx) }`,
|
||||
src: accountRowSource + `fun query(row: AccountRow, pool: Any, ctx: Any): List<*AccountRow> { return sql.insert<AccountRow>(row).fetch(pool, ctx).unwrap() }`,
|
||||
want: "fetch() is only supported for sql.from",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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_>])\\?"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue