Add structured coroutines and explicit error handling

This commit is contained in:
pavel 2026-08-27 16:26:40 +02:00
commit fe62e81152
31 changed files with 1701 additions and 325 deletions

View file

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

View file

@ -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,9 +1106,11 @@ fun writer(ch: Channel<Int>) {
fun main() {
val ch = Channel<Int>()
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) {

124
internal/lang/coroutines.go Normal file
View 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
}

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

View file

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

View file

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

View file

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

View file

@ -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('|') {

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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