Infer coroutine effects automatically

This commit is contained in:
pavel 2026-08-27 21:53:34 +02:00
commit 5b30e49486
15 changed files with 339 additions and 103 deletions

View file

@ -33,7 +33,7 @@ the semantic Type-to-Go mapping turns a class such as `User` into `*User`.
- Gotlin classes are reference types by default; `*` is only needed for external Go pointer types - 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)` - 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` - 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` - inferred structured coroutine effects with `runBlocking`, `coroutineScope`, `launch`, `async`, `await`, `delay`, `withTimeout`, `isActive`, and `coroutineContext`
## Example ## Example
@ -282,8 +282,18 @@ sibling coroutine contexts. The removed `worker`, bare `go`, and channel
`select` forms are not valid Gotlin syntax; use coroutine scopes, `delay`, and `select` forms are not valid Gotlin syntax; use coroutine scopes, `delay`, and
explicit channel `read()`/`send()` operations. explicit channel `read()`/`send()` operations.
Coroutine effects are inferred through the call graph. Functions that directly
or transitively use coroutine operations receive a hidden scope parameter and
can only be called from an ambient coroutine scope. `runBlocking` establishes a
scope boundary, so neither a `suspend` modifier nor manually threaded context is
needed.
Use `coroutineContext()` only at Go interop boundaries that require a
`context.Context`; it returns the ambient scope context without exposing it in
the Gotlin function signature.
```kotlin ```kotlin
suspend fun load(): Int { fun load(): Int {
delay(10) delay(10)
return 42 return 42
} }

View file

@ -1583,40 +1583,41 @@ func builtinHoverDetail(name string) (string, bool) {
} }
var builtinDetails = map[string]string{ var builtinDetails = map[string]string{
"println": "fun println(value: Any): Unit", "println": "fun println(value: Any): Unit",
"runCatching": "fun runCatching(block: () -> Unit): Result", "runCatching": "fun runCatching(block: () -> Unit): Result",
"Channel": "fun Channel<T>(capacity: Int = 0): Channel<T>", "Channel": "fun Channel<T>(capacity: Int = 0): Channel<T>",
"listOf": "fun listOf<T>(values: T...): List<T>", "listOf": "fun listOf<T>(values: T...): List<T>",
"mutableListOf": "fun mutableListOf<T>(values: T...): MutableList<T>", "mutableListOf": "fun mutableListOf<T>(values: T...): MutableList<T>",
"mapOf": "fun mapOf<K, V>(pairs: Any...): Map<K, V>", "mapOf": "fun mapOf<K, V>(pairs: Any...): Map<K, V>",
"mutableMapOf": "fun mutableMapOf<K, V>(pairs: Any...): MutableMap<K, V>", "mutableMapOf": "fun mutableMapOf<K, V>(pairs: Any...): MutableMap<K, V>",
"ByteSlice": "fun ByteSlice(value: String | Int): ByteSlice", "ByteSlice": "fun ByteSlice(value: String | Int): ByteSlice",
"append": "fun append<T>(values: List<T>, value: T): List<T>", "append": "fun append<T>(values: List<T>, value: T): List<T>",
"keys": "fun keys<K, V>(values: Map<K, V>): List<K>", "keys": "fun keys<K, V>(values: Map<K, V>): List<K>",
"goAssert": "fun goAssert<T>(value: Any): T", "goAssert": "fun goAssert<T>(value: Any): T",
"len": "fun len(value: Any): Int", "len": "fun len(value: Any): Int",
"cap": "fun cap(value: Any): Int", "cap": "fun cap(value: Any): Int",
"make": "fun make<T>(size: Int): T", "make": "fun make<T>(size: Int): T",
"new": "fun new<T>(): *T", "new": "fun new<T>(): *T",
"copy": "fun copy(target: Any, source: Any): Int", "copy": "fun copy(target: Any, source: Any): Int",
"delete": "fun delete(map: Any, key: Any): Unit", "delete": "fun delete(map: Any, key: Any): Unit",
"close": "fun close(channel: Any): Unit", "close": "fun close(channel: Any): Unit",
"panic": "fun panic(value: Any): Unit", "panic": "fun panic(value: Any): Unit",
"recover": "fun recover(): Any", "recover": "fun recover(): Any",
"string": "fun string(value: Any): String", "string": "fun string(value: Any): String",
"int": "fun int(value: Any): Int", "int": "fun int(value: Any): Int",
"float64": "fun float64(value: Any): Double", "float64": "fun float64(value: Any): Double",
"bool": "fun bool(value: Any): Boolean", "bool": "fun bool(value: Any): Boolean",
"sql": "typed PostgreSQL query DSL", "sql": "typed PostgreSQL query DSL",
"set": "fun set(target: Any, value: Any): Unit", "set": "fun set(target: Any, value: Any): Unit",
"now": "fun now(): time.Time", "now": "fun now(): time.Time",
"runBlocking": "fun runBlocking(block: suspend () -> Unit): Unit", "runBlocking": "fun runBlocking(block: () -> Unit): Unit",
"coroutineScope": "suspend fun coroutineScope(block: suspend () -> Unit): Unit", "coroutineScope": "contextual fun coroutineScope(block: () -> Unit): Unit",
"launch": "suspend fun launch(block: suspend () -> Unit): Unit", "launch": "contextual fun launch(block: () -> Unit): Unit",
"async": "suspend fun async<T>(block: suspend () -> T): Deferred<T>", "async": "contextual fun async<T>(block: () -> T): Deferred<T>",
"delay": "suspend fun delay(ms: Int): Unit", "delay": "contextual fun delay(ms: Int): Unit",
"withTimeout": "suspend fun withTimeout(ms: Int, block: suspend () -> Unit): Unit", "withTimeout": "contextual fun withTimeout(ms: Int, block: () -> Unit): Unit",
"isActive": "suspend fun isActive(): Boolean", "isActive": "contextual fun isActive(): Boolean",
"coroutineContext": "contextual fun coroutineContext(): context.Context",
} }
func contains(values []string, needle string) bool { func contains(values []string, needle string) bool {

View file

@ -65,7 +65,6 @@ type FunctionSignature struct {
Params []Param Params []Param
ReturnType string ReturnType string
ReturnRef TypeRef ReturnRef TypeRef
Suspend bool
} }
type FunctionDecl struct { type FunctionDecl struct {
@ -75,7 +74,6 @@ type FunctionDecl struct {
ReturnType string ReturnType string
ReturnRef TypeRef ReturnRef TypeRef
Body []Stmt Body []Stmt
Suspend bool
} }
type Param struct { type Param struct {

View file

@ -5,17 +5,17 @@ import (
"strings" "strings"
) )
var coroutineBuiltins = map[string]bool{"runBlocking": true, "coroutineScope": true, "launch": true, "async": true, "delay": true, "withTimeout": true, "isActive": true} var coroutineBuiltins = map[string]bool{"runBlocking": true, "coroutineScope": true, "launch": true, "async": true, "delay": true, "withTimeout": true, "isActive": true, "coroutineContext": true}
func programUsesCoroutines(program *Program) bool { func programUsesCoroutines(program *Program) bool {
for _, fn := range program.Functions { for _, fn := range program.Functions {
if fn.Suspend || statementsUseCoroutines(fn.Body) { if statementsUseCoroutines(fn.Body) {
return true return true
} }
} }
for _, class := range program.Classes { for _, class := range program.Classes {
for _, fn := range class.Methods { for _, fn := range class.Methods {
if fn.Suspend || statementsUseCoroutines(fn.Body) { if statementsUseCoroutines(fn.Body) {
return true return true
} }
} }
@ -94,6 +94,7 @@ func (g *goGenerator) emitCoroutineSupport() {
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) 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) 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) IsActive() bool { return scope.ctx.Err()==nil }")
g.line("func (scope *GotlinCoroutineScope) Context() context.Context { return scope.ctx }")
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 (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("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("type GotlinDeferred[T any] struct { done chan struct{}; value T; failure any }")

View file

@ -7,7 +7,7 @@ import (
func TestGenerateStructuredCoroutines(t *testing.T) { func TestGenerateStructuredCoroutines(t *testing.T) {
prog, err := Parse(`package demo prog, err := Parse(`package demo
suspend fun load(): Int { delay(1); return 42 } fun load(): Int { delay(1); return 42 }
fun main() { fun main() {
runBlocking { runBlocking {
coroutineScope { coroutineScope {
@ -31,16 +31,35 @@ fun main() {
} }
} }
func TestSuspendFunctionRequiresScope(t *testing.T) { func TestInferredCoroutineFunctionRequiresScope(t *testing.T) {
prog, err := Parse(`package demo prog, err := Parse(`package demo
suspend fun load(): Int { return 1 } fun load(): Int { delay(1); return 1 }
fun main() { println(load()) }`) fun main() { println(load()) }`)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err = GenerateGo(prog) out, err := GenerateGo(prog)
if err == nil || !strings.Contains(err.Error(), "requires a coroutine scope") { if err == nil || !strings.Contains(err.Error(), "contextual function main requires a runBlocking boundary") {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v\n%s", err, out)
}
}
func TestCoroutineEffectPropagatesTransitively(t *testing.T) {
prog, err := Parse(`package demo
fun load(): Int { delay(1); return 42 }
fun wrapped(): Int { return load() }
fun main() { runBlocking { println(wrapped()) } }`)
if err != nil {
t.Fatal(err)
}
out, err := GenerateGo(prog)
if err != nil {
t.Fatal(err)
}
for _, expected := range []string{"func load(gotlinScope *GotlinCoroutineScope)", "func wrapped(gotlinScope *GotlinCoroutineScope)", "wrapped(gotlinScope)"} {
if !strings.Contains(string(out), expected) {
t.Fatalf("missing %q:\n%s", expected, out)
}
} }
} }
@ -52,6 +71,51 @@ func TestLegacyConcurrencySyntaxIsRemoved(t *testing.T) {
} }
} }
func TestSuspendModifierIsRemoved(t *testing.T) {
if _, err := Parse(`package demo suspend fun load() {}`); err == nil {
t.Fatal("suspend modifier parsed")
}
}
func TestAnalyzeInfersCoroutineEffects(t *testing.T) {
program, err := Parse(`package demo
fun direct() { delay(1) }
fun transitive() { direct() }
fun boundary() { runBlocking { transitive() } }`)
if err != nil {
t.Fatal(err)
}
semantic, err := Analyze(program)
if err != nil {
t.Fatal(err)
}
if !semantic.FunctionEffects["direct"].Has(CoroutineEffect) || !semantic.FunctionEffects["transitive"].Has(CoroutineEffect) {
t.Fatalf("effects were not propagated: %#v", semantic.FunctionEffects)
}
if semantic.FunctionEffects["boundary"].Has(CoroutineEffect) {
t.Fatalf("runBlocking did not stop effect propagation: %#v", semantic.FunctionEffects)
}
}
func TestCoroutineContextUsesAmbientScope(t *testing.T) {
program, err := Parse(`package demo
import fmt
fun useContext() { fmt.sprint(coroutineContext()) }
fun main() { runBlocking { useContext() } }`)
if err != nil {
t.Fatal(err)
}
output, err := GenerateGo(program)
if err != nil {
t.Fatal(err)
}
for _, expected := range []string{"func useContext(gotlinScope *GotlinCoroutineScope)", "gotlinScope.Context()", "useContext(gotlinScope)"} {
if !strings.Contains(string(output), expected) {
t.Fatalf("missing %q:\n%s", expected, output)
}
}
}
func TestRunBlockingCancelsAndJoinsChildrenOnPanic(t *testing.T) { func TestRunBlockingCancelsAndJoinsChildrenOnPanic(t *testing.T) {
prog, err := Parse(`package demo fun main() { runBlocking { launch { delay(1) }; panic("failed") } }`) prog, err := Parse(`package demo fun main() { runBlocking { launch { delay(1) }; panic("failed") } }`)
if err != nil { if err != nil {

155
internal/lang/effects.go Normal file
View file

@ -0,0 +1,155 @@
package lang
type Effect uint8
const (
NoEffect Effect = 0
CoroutineEffect Effect = 1 << 0
)
func (effect Effect) Has(required Effect) bool { return effect&required != 0 }
type effectNode struct {
key string
symbol *Symbol
direct Effect
callees []*Symbol
}
func inferCoroutineEffects(semantic *SemanticProgram) {
var nodes []*effectNode
for index := range semantic.Syntax.Functions {
decl := &semantic.Syntax.Functions[index]
symbol, _ := semantic.Global.Lookup(decl.Name)
node := &effectNode{key: decl.Name, symbol: symbol}
collectFunctionEffects(decl.Body, node)
nodes = append(nodes, node)
}
for classIndex := range semantic.Syntax.Classes {
class := &semantic.Syntax.Classes[classIndex]
classSymbol := semantic.ClassInfo[class.Name]
for methodIndex := range class.Methods {
method := &class.Methods[methodIndex]
node := &effectNode{key: class.Name + "." + method.Name, symbol: classSymbol.Methods[method.Name]}
collectFunctionEffects(method.Body, node)
nodes = append(nodes, node)
}
}
changed := true
for changed {
changed = false
for _, node := range nodes {
effects := node.direct
for _, callee := range node.callees {
effects |= callee.Effects
}
if node.symbol != nil && node.symbol.Effects != effects {
node.symbol.Effects = effects
changed = true
}
}
}
for _, node := range nodes {
if node.symbol != nil {
semantic.FunctionEffects[node.key] = node.symbol.Effects
if function, ok := node.symbol.Type.(FunctionType); ok {
function.Effects = node.symbol.Effects
node.symbol.Type = function
}
}
}
}
func collectFunctionEffects(statements []Stmt, node *effectNode) {
for _, statement := range statements {
switch value := statement.(type) {
case VarDecl:
collectExpressionEffects(value.Value, node)
case MultiVarDecl:
collectExpressionEffects(value.Value, node)
case AssignStmt:
collectExpressionEffects(value.Value, node)
case AddAssignStmt:
collectExpressionEffects(value.Value, node)
case MultiAssignStmt:
collectExpressionEffects(value.Value, node)
case ReturnStmt:
if value.Value != nil {
collectExpressionEffects(value.Value, node)
}
case ThrowStmt:
collectExpressionEffects(value.Value, node)
case DeferStmt:
collectExpressionEffects(value.Value, node)
case ExprStmt:
collectExpressionEffects(value.Value, node)
case IfStmt:
collectExpressionEffects(value.Cond, node)
collectFunctionEffects(value.Then, node)
collectFunctionEffects(value.Else, node)
case WhileStmt:
collectExpressionEffects(value.Cond, node)
collectFunctionEffects(value.Body, node)
case ForEachStmt:
collectExpressionEffects(value.Source, node)
collectFunctionEffects(value.Body, node)
case MatchStmt:
collectExpressionEffects(value.Value, node)
for _, matchCase := range value.Cases {
collectFunctionEffects(matchCase.Body, node)
}
case TryCatchStmt:
collectFunctionEffects(value.TryBody, node)
collectFunctionEffects(value.CatchBody, node)
}
}
}
func collectExpressionEffects(expression Expr, node *effectNode) {
switch value := expression.(type) {
case CallExpr:
if ident, ok := value.Callee.(IdentExpr); ok {
if ident.Name == "runBlocking" {
return
}
if coroutineBuiltins[ident.Name] {
node.direct |= CoroutineEffect
}
}
if semantic := exprMeta(value); semantic != nil {
if call, ok := semantic.Node.(HIRGotlinCall); ok && call.Target != nil {
node.callees = append(node.callees, call.Target)
}
}
collectExpressionEffects(value.Callee, node)
for _, argument := range value.Args {
collectExpressionEffects(argument, node)
}
for _, argument := range value.NamedArgs {
collectExpressionEffects(argument.Value, node)
}
case UnaryExpr:
collectExpressionEffects(value.Value, node)
case NonNullExpr:
collectExpressionEffects(value.Value, node)
case TryExpr:
collectExpressionEffects(value.Value, node)
case BinaryExpr:
collectExpressionEffects(value.Left, node)
collectExpressionEffects(value.Right, node)
case SelectorExpr:
collectExpressionEffects(value.Receiver, node)
case SafeSelectorExpr:
collectExpressionEffects(value.Receiver, node)
case IndexExpr:
collectExpressionEffects(value.Receiver, node)
collectExpressionEffects(value.Index, node)
case MatchExpr:
collectExpressionEffects(value.Value, node)
for _, matchCase := range value.Cases {
collectExpressionEffects(matchCase.Value, node)
}
case LambdaExpr:
collectFunctionEffects(value.Body, node)
}
}

View file

@ -251,7 +251,11 @@ func (g *goGenerator) function(fn FunctionDecl) error {
g.currentClass = nil g.currentClass = nil
g.currentFunc = fn g.currentFunc = fn
previousScope := g.currentCoroutineScope previousScope := g.currentCoroutineScope
if fn.Suspend { hasCoroutineEffect := g.hasCoroutineEffect(fn)
if hasCoroutineEffect && fn.Name == "main" {
return fmt.Errorf("contextual function main requires a runBlocking boundary")
}
if hasCoroutineEffect {
g.currentCoroutineScope = "gotlinScope" g.currentCoroutineScope = "gotlinScope"
} }
defer func() { g.currentCoroutineScope = previousScope }() defer func() { g.currentCoroutineScope = previousScope }()
@ -284,11 +288,7 @@ func (g *goGenerator) interfaceDecl(decl InterfaceDecl) {
g.line("type " + decl.Name + " interface {") g.line("type " + decl.Name + " interface {")
g.indentLevel++ g.indentLevel++
for _, method := range decl.Methods { for _, method := range decl.Methods {
prefix := "" g.line(method.Name + g.renderGoParamsWithPrefix(method.Params, "") + g.renderGoReturnSuffix(method.ReturnType))
if method.Suspend {
prefix = "gotlinScope *GotlinCoroutineScope"
}
g.line(method.Name + g.renderGoParamsWithPrefix(method.Params, prefix) + g.renderGoReturnSuffix(method.ReturnType))
} }
g.indentLevel-- g.indentLevel--
g.line("}") g.line("}")
@ -416,7 +416,7 @@ func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error {
g.currentClass = &class g.currentClass = &class
g.currentFunc = fn g.currentFunc = fn
previousScope := g.currentCoroutineScope previousScope := g.currentCoroutineScope
if fn.Suspend { if g.hasCoroutineEffect(fn) {
g.currentCoroutineScope = "gotlinScope" g.currentCoroutineScope = "gotlinScope"
} }
defer func() { g.currentCoroutineScope = previousScope }() defer func() { g.currentCoroutineScope = previousScope }()
@ -954,6 +954,11 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) {
return "", fmt.Errorf("isActive requires a coroutine scope") return "", fmt.Errorf("isActive requires a coroutine scope")
} }
return g.currentCoroutineScope + ".IsActive()", nil return g.currentCoroutineScope + ".IsActive()", nil
case "coroutineContext":
if g.currentCoroutineScope == "" {
return "", fmt.Errorf("coroutineContext requires a coroutine scope")
}
return g.currentCoroutineScope + ".Context()", nil
} }
} }
if selector, ok := e.Callee.(SelectorExpr); ok && selector.Name == "await" && len(e.Args) == 0 { if selector, ok := e.Callee.(SelectorExpr); ok && selector.Name == "await" && len(e.Args) == 0 {
@ -1197,27 +1202,14 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) {
return fmt.Sprintf("New%s%s(%s)", ident.Name, g.renderCallTypeArguments(e.TypeArgs), strings.Join(args, ", ")), nil return fmt.Sprintf("New%s%s(%s)", ident.Name, g.renderCallTypeArguments(e.TypeArgs), strings.Join(args, ", ")), nil
} }
} }
if ident, ok := e.Callee.(IdentExpr); ok { if resolvedCall != nil {
if fn, found := g.semantic.Functions[ident.Name]; found && fn.Suspend { if call, ok := resolvedCall.Node.(HIRGotlinCall); ok && call.Target != nil && call.Target.Effects.Has(CoroutineEffect) {
if g.currentCoroutineScope == "" { if g.currentCoroutineScope == "" {
return "", fmt.Errorf("suspend function %s requires a coroutine scope", ident.Name) return "", fmt.Errorf("contextual function %s requires a coroutine scope", call.Target.Name)
} }
args = append([]string{g.currentCoroutineScope}, args...) args = append([]string{g.currentCoroutineScope}, args...)
} }
} }
if selector, ok := e.Callee.(SelectorExpr); ok {
if class, found := g.classForType(g.exprType(selector.Receiver)); found {
for _, method := range class.Methods {
if method.Name == selector.Name && method.Suspend {
if g.currentCoroutineScope == "" {
return "", fmt.Errorf("suspend method %s requires a coroutine scope", selector.Name)
}
args = append([]string{g.currentCoroutineScope}, args...)
break
}
}
}
}
callee, err := g.expr(e.Callee, "") callee, err := g.expr(e.Callee, "")
if err != nil { if err != nil {
return "", err return "", err
@ -2491,12 +2483,20 @@ func (g *goGenerator) cloneScopes() []map[string]bool {
func (g *goGenerator) renderGoFunctionParams(function FunctionDecl) string { func (g *goGenerator) renderGoFunctionParams(function FunctionDecl) string {
prefix := "" prefix := ""
if function.Suspend { if g.hasCoroutineEffect(function) {
prefix = "gotlinScope *GotlinCoroutineScope" prefix = "gotlinScope *GotlinCoroutineScope"
} }
return g.renderGoParamsWithPrefix(function.Params, prefix) return g.renderGoParamsWithPrefix(function.Params, prefix)
} }
func (g *goGenerator) hasCoroutineEffect(function FunctionDecl) bool {
key := function.Name
if g.currentClass != nil {
key = g.currentClass.Name + "." + function.Name
}
return g.semantic.FunctionEffects[key].Has(CoroutineEffect)
}
func renderGoTypeParameters(params []string) string { func renderGoTypeParameters(params []string) string {
if len(params) == 0 { if len(params) == 0 {
return "" return ""

View file

@ -600,6 +600,11 @@ func (resolver *semanticResolver) meaning(expr Expr, scope *Scope) (ExprMeaning,
if root, ok := selectorRootAlias(selector); ok && resolver.program.Imports[root] { if root, ok := selectorRootAlias(selector); ok && resolver.program.Imports[root] {
return GoCallExpr, nil return GoCallExpr, nil
} }
if class, _ := classInstance(ResolvedType(selector.Receiver)); class != nil {
if method, ok := class.Methods[selector.Name]; ok {
return MethodCallExpr, method
}
}
receiverType := ResolvedType(selector.Receiver) receiverType := ResolvedType(selector.Receiver)
if !isUnknownType(resolver.program.goMethodType(receiverType, selector.Name)) { if !isUnknownType(resolver.program.goMethodType(receiverType, selector.Name)) {
return GoCallExpr, nil return GoCallExpr, nil
@ -755,7 +760,7 @@ var semanticBuiltins = map[string]bool{
"panic": true, "recover": true, "string": true, "int": true, "float64": true, "bool": true, "panic": true, "recover": true, "string": true, "int": true, "float64": true, "bool": true,
"sql": true, "set": true, "now": true, "Result": true, "ByteSlice": true, "sql": true, "set": true, "now": true, "Result": true, "ByteSlice": true,
"runBlocking": true, "coroutineScope": true, "launch": true, "async": true, "runBlocking": true, "coroutineScope": true, "launch": true, "async": true,
"delay": true, "withTimeout": true, "isActive": true, "delay": true, "withTimeout": true, "isActive": true, "coroutineContext": true,
"continue": true, "break": true, "continue": true, "break": true,
} }

View file

@ -138,7 +138,7 @@ func (p *parser) parseProgram() (*Program, error) {
default: default:
return nil, fmt.Errorf("unsupported annotation %q", annotation.lexeme) return nil, fmt.Errorf("unsupported annotation %q", annotation.lexeme)
} }
case p.check(tokenFun) || p.check(tokenSuspend): case p.check(tokenFun):
fn, err := p.parseFunction() fn, err := p.parseFunction()
if err != nil { if err != nil {
return nil, err return nil, err
@ -325,7 +325,7 @@ func (p *parser) parseClass() (ClassDecl, error) {
var methods []FunctionDecl var methods []FunctionDecl
for !p.check(tokenRBrace) && !p.check(tokenEOF) { for !p.check(tokenRBrace) && !p.check(tokenEOF) {
p.match(tokenOverride) p.match(tokenOverride)
if !p.check(tokenFun) && !p.check(tokenSuspend) { if !p.check(tokenFun) {
tok := p.peek() tok := p.peek()
return ClassDecl{}, fmt.Errorf("expected class member at %d, found %q", tok.pos, tok.lexeme) return ClassDecl{}, fmt.Errorf("expected class member at %d, found %q", tok.pos, tok.lexeme)
} }
@ -482,12 +482,10 @@ func (p *parser) parseFunction() (FunctionDecl, error) {
Params: signature.Params, Params: signature.Params,
ReturnType: signature.ReturnType, ReturnType: signature.ReturnType,
Body: body, Body: body,
Suspend: signature.Suspend,
}, nil }, nil
} }
func (p *parser) parseFunctionSignature() (FunctionSignature, error) { func (p *parser) parseFunctionSignature() (FunctionSignature, error) {
suspend := p.match(tokenSuspend)
if _, err := p.expect(tokenFun, "expected 'fun'"); err != nil { if _, err := p.expect(tokenFun, "expected 'fun'"); err != nil {
return FunctionSignature{}, err return FunctionSignature{}, err
} }
@ -524,7 +522,6 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) {
TypeParams: typeParams, TypeParams: typeParams,
Params: params, Params: params,
ReturnType: returnType, ReturnType: returnType,
Suspend: suspend,
}, nil }, nil
} }

View file

@ -21,6 +21,7 @@ type Symbol struct {
Type Type Type Type
Mutable bool Mutable bool
Decl any Decl any
Effects Effect
} }
type Scope struct { type Scope struct {
@ -56,17 +57,18 @@ type ClassSymbol struct {
} }
type SemanticProgram struct { type SemanticProgram struct {
Syntax *Program Syntax *Program
Global *Scope Global *Scope
Classes map[string]ClassDecl Classes map[string]ClassDecl
ClassInfo map[string]*ClassSymbol ClassInfo map[string]*ClassSymbol
Functions map[string]FunctionDecl Functions map[string]FunctionDecl
Enums map[string]EnumDecl Enums map[string]EnumDecl
Imports map[string]bool Imports map[string]bool
GoPackages map[string]*gotypes.Package GoPackages map[string]*gotypes.Package
HIR *HIRProgram HIR *HIRProgram
Mappings *mappingState Mappings *mappingState
Diagnostics []SemanticDiagnostic Diagnostics []SemanticDiagnostic
FunctionEffects map[string]Effect
} }
func Analyze(program *Program) (*SemanticProgram, error) { func Analyze(program *Program) (*SemanticProgram, error) {
@ -91,15 +93,16 @@ func AnalyzeWithContext(program *Program, additional []*Program) (*SemanticProgr
func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, error) { func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, error) {
semantic := &SemanticProgram{ semantic := &SemanticProgram{
Syntax: program, Syntax: program,
Global: NewScope(nil), Global: NewScope(nil),
Classes: map[string]ClassDecl{}, Classes: map[string]ClassDecl{},
ClassInfo: map[string]*ClassSymbol{}, ClassInfo: map[string]*ClassSymbol{},
Functions: map[string]FunctionDecl{}, Functions: map[string]FunctionDecl{},
Enums: map[string]EnumDecl{}, Enums: map[string]EnumDecl{},
Imports: map[string]bool{}, Imports: map[string]bool{},
GoPackages: map[string]*gotypes.Package{}, GoPackages: map[string]*gotypes.Package{},
Mappings: &mappingState{functions: map[string]string{}}, Mappings: &mappingState{functions: map[string]string{}},
FunctionEffects: map[string]Effect{},
} }
programs := append([]*Program{program}, additional...) programs := append([]*Program{program}, additional...)
for _, source := range programs { for _, source := range programs {
@ -198,6 +201,7 @@ func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram,
if err := resolver.resolve(); err != nil { if err := resolver.resolve(); err != nil {
return nil, err return nil, err
} }
inferCoroutineEffects(semantic)
return semantic, nil return semantic, nil
} }

View file

@ -19,7 +19,6 @@ const (
tokenEnum tokenKind = "ENUM" tokenEnum tokenKind = "ENUM"
tokenMatch tokenKind = "MATCH" tokenMatch tokenKind = "MATCH"
tokenFun tokenKind = "FUN" tokenFun tokenKind = "FUN"
tokenSuspend tokenKind = "SUSPEND"
tokenOverride tokenKind = "OVERRIDE" tokenOverride tokenKind = "OVERRIDE"
tokenPrivate tokenKind = "PRIVATE" tokenPrivate tokenKind = "PRIVATE"
tokenVal tokenKind = "VAL" tokenVal tokenKind = "VAL"
@ -70,7 +69,6 @@ const (
var keywords = map[string]tokenKind{ var keywords = map[string]tokenKind{
"fun": tokenFun, "fun": tokenFun,
"suspend": tokenSuspend,
"import": tokenImport, "import": tokenImport,
"package": tokenPackage, "package": tokenPackage,
"class": tokenClass, "class": tokenClass,

View file

@ -125,6 +125,8 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment)
if len(value.TypeArgs) == 1 { if len(value.TypeArgs) == 1 {
return GenericType{Base: NamedType{Name: "Deferred"}, Args: []Type{resolve(value.TypeArgs[0])}} return GenericType{Base: NamedType{Name: "Deferred"}, Args: []Type{resolve(value.TypeArgs[0])}}
} }
case "coroutineContext":
return NamedType{Name: "context.Context"}
} }
if ident.Name == "keys" && len(value.Args) == 1 { if ident.Name == "keys" && len(value.Args) == 1 {
if mapping, ok := semantic.TypeOf(value.Args[0], environment).(GenericType); ok && (mapping.Base.String() == "Map" || mapping.Base.String() == "MutableMap") && len(mapping.Args) == 2 { if mapping, ok := semantic.TypeOf(value.Args[0], environment).(GenericType); ok && (mapping.Base.String() == "Map" || mapping.Base.String() == "MutableMap") && len(mapping.Args) == 2 {

View file

@ -57,6 +57,7 @@ type FunctionType struct {
TypeParams []string TypeParams []string
Params []Type Params []Type
Result Type Result Type
Effects Effect
} }
func (FunctionType) typeNode() {} func (FunctionType) typeNode() {}
@ -200,7 +201,7 @@ func resolveTypeParameters(typ Type, params map[string]bool) Type {
for index, param := range value.Params { for index, param := range value.Params {
resolved[index] = resolveTypeParameters(param, params) resolved[index] = resolveTypeParameters(param, params)
} }
return FunctionType{TypeParams: value.TypeParams, Params: resolved, Result: resolveTypeParameters(value.Result, params)} return FunctionType{TypeParams: value.TypeParams, Params: resolved, Result: resolveTypeParameters(value.Result, params), Effects: value.Effects}
case GenericType: case GenericType:
args := make([]Type, len(value.Args)) args := make([]Type, len(value.Args))
for index, arg := range value.Args { for index, arg := range value.Args {
@ -248,7 +249,7 @@ func substituteType(typ Type, bindings map[string]Type) Type {
for index, param := range value.Params { for index, param := range value.Params {
params[index] = substituteType(param, bindings) params[index] = substituteType(param, bindings)
} }
return FunctionType{TypeParams: value.TypeParams, Params: params, Result: substituteType(value.Result, bindings)} return FunctionType{TypeParams: value.TypeParams, Params: params, Result: substituteType(value.Result, bindings), Effects: value.Effects}
default: default:
return typ return typ
} }

View file

@ -42,7 +42,7 @@ assert(language.folding?.markers?.start && language.indentationRules?.increaseIn
const grammarSource = JSON.stringify(grammar); const grammarSource = JSON.stringify(grammar);
const expectedTokens = [ const expectedTokens = [
"data", "class", "suspend", "private", "override", "val", "var", "if", "else", "data", "class", "private", "override", "val", "var", "if", "else",
"while", "for", "in", "return", "defer", "try", "catch", "while", "for", "in", "return", "defer", "try", "catch",
"throw", "enum", "match", "package", "import", "jsonNaming", "embed", "table", "column", "id", "throw", "enum", "match", "package", "import", "jsonNaming", "embed", "table", "column", "id",
"generated", "Int", "Long", "String", "Boolean", "Unit", "Double", "Float", "Any", "generated", "Int", "Long", "String", "Boolean", "Unit", "Double", "Float", "Any",

View file

@ -254,7 +254,7 @@
"patterns": [ "patterns": [
{ {
"name": "keyword.control.declaration.gotlin", "name": "keyword.control.declaration.gotlin",
"match": "\\b(package|import|data|class|interface|enum|suspend|fun)\\b" "match": "\\b(package|import|data|class|interface|enum|fun)\\b"
}, },
{ {
"name": "storage.modifier.gotlin", "name": "storage.modifier.gotlin",