From 5b30e4948652da432d539d031de8fb118b42c0af Mon Sep 17 00:00:00 2001 From: pavel Date: Thu, 27 Aug 2026 21:53:34 +0200 Subject: [PATCH] Infer coroutine effects automatically --- README.md | 14 +- cmd/gotlin-lsp/main.go | 69 ++++---- internal/lang/ast.go | 2 - internal/lang/coroutines.go | 7 +- internal/lang/coroutines_test.go | 76 ++++++++- internal/lang/effects.go | 155 ++++++++++++++++++ internal/lang/generate_go.go | 48 +++--- internal/lang/hir.go | 7 +- internal/lang/parser.go | 7 +- internal/lang/symbols.go | 44 ++--- internal/lang/token.go | 2 - internal/lang/type_checker.go | 2 + internal/lang/types.go | 5 +- tools/vscode-gotlin/scripts/validate.js | 2 +- .../syntaxes/gotlin.tmLanguage.json | 2 +- 15 files changed, 339 insertions(+), 103 deletions(-) create mode 100644 internal/lang/effects.go diff --git a/README.md b/README.md index a60665f..16d8faf 100644 --- a/README.md +++ b/README.md @@ -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 - 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` +- inferred structured coroutine effects with `runBlocking`, `coroutineScope`, `launch`, `async`, `await`, `delay`, `withTimeout`, `isActive`, and `coroutineContext` ## 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 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 -suspend fun load(): Int { +fun load(): Int { delay(10) return 42 } diff --git a/cmd/gotlin-lsp/main.go b/cmd/gotlin-lsp/main.go index 790257d..bddf14b 100644 --- a/cmd/gotlin-lsp/main.go +++ b/cmd/gotlin-lsp/main.go @@ -1583,40 +1583,41 @@ func builtinHoverDetail(name string) (string, bool) { } var builtinDetails = map[string]string{ - "println": "fun println(value: Any): Unit", - "runCatching": "fun runCatching(block: () -> Unit): Result", - "Channel": "fun Channel(capacity: Int = 0): Channel", - "listOf": "fun listOf(values: T...): List", - "mutableListOf": "fun mutableListOf(values: T...): MutableList", - "mapOf": "fun mapOf(pairs: Any...): Map", - "mutableMapOf": "fun mutableMapOf(pairs: Any...): MutableMap", - "ByteSlice": "fun ByteSlice(value: String | Int): ByteSlice", - "append": "fun append(values: List, value: T): List", - "keys": "fun keys(values: Map): List", - "goAssert": "fun goAssert(value: Any): T", - "len": "fun len(value: Any): Int", - "cap": "fun cap(value: Any): Int", - "make": "fun make(size: Int): T", - "new": "fun new(): *T", - "copy": "fun copy(target: Any, source: Any): Int", - "delete": "fun delete(map: Any, key: Any): Unit", - "close": "fun close(channel: Any): Unit", - "panic": "fun panic(value: Any): Unit", - "recover": "fun recover(): Any", - "string": "fun string(value: Any): String", - "int": "fun int(value: Any): Int", - "float64": "fun float64(value: Any): Double", - "bool": "fun bool(value: Any): Boolean", - "sql": "typed PostgreSQL query DSL", - "set": "fun set(target: Any, value: Any): Unit", - "now": "fun now(): time.Time", - "runBlocking": "fun runBlocking(block: suspend () -> Unit): Unit", - "coroutineScope": "suspend fun coroutineScope(block: suspend () -> Unit): Unit", - "launch": "suspend fun launch(block: suspend () -> Unit): Unit", - "async": "suspend fun async(block: suspend () -> T): Deferred", - "delay": "suspend fun delay(ms: Int): Unit", - "withTimeout": "suspend fun withTimeout(ms: Int, block: suspend () -> Unit): Unit", - "isActive": "suspend fun isActive(): Boolean", + "println": "fun println(value: Any): Unit", + "runCatching": "fun runCatching(block: () -> Unit): Result", + "Channel": "fun Channel(capacity: Int = 0): Channel", + "listOf": "fun listOf(values: T...): List", + "mutableListOf": "fun mutableListOf(values: T...): MutableList", + "mapOf": "fun mapOf(pairs: Any...): Map", + "mutableMapOf": "fun mutableMapOf(pairs: Any...): MutableMap", + "ByteSlice": "fun ByteSlice(value: String | Int): ByteSlice", + "append": "fun append(values: List, value: T): List", + "keys": "fun keys(values: Map): List", + "goAssert": "fun goAssert(value: Any): T", + "len": "fun len(value: Any): Int", + "cap": "fun cap(value: Any): Int", + "make": "fun make(size: Int): T", + "new": "fun new(): *T", + "copy": "fun copy(target: Any, source: Any): Int", + "delete": "fun delete(map: Any, key: Any): Unit", + "close": "fun close(channel: Any): Unit", + "panic": "fun panic(value: Any): Unit", + "recover": "fun recover(): Any", + "string": "fun string(value: Any): String", + "int": "fun int(value: Any): Int", + "float64": "fun float64(value: Any): Double", + "bool": "fun bool(value: Any): Boolean", + "sql": "typed PostgreSQL query DSL", + "set": "fun set(target: Any, value: Any): Unit", + "now": "fun now(): time.Time", + "runBlocking": "fun runBlocking(block: () -> Unit): Unit", + "coroutineScope": "contextual fun coroutineScope(block: () -> Unit): Unit", + "launch": "contextual fun launch(block: () -> Unit): Unit", + "async": "contextual fun async(block: () -> T): Deferred", + "delay": "contextual fun delay(ms: Int): Unit", + "withTimeout": "contextual fun withTimeout(ms: Int, block: () -> Unit): Unit", + "isActive": "contextual fun isActive(): Boolean", + "coroutineContext": "contextual fun coroutineContext(): context.Context", } func contains(values []string, needle string) bool { diff --git a/internal/lang/ast.go b/internal/lang/ast.go index f8c7a5b..0f4e7c8 100644 --- a/internal/lang/ast.go +++ b/internal/lang/ast.go @@ -65,7 +65,6 @@ type FunctionSignature struct { Params []Param ReturnType string ReturnRef TypeRef - Suspend bool } type FunctionDecl struct { @@ -75,7 +74,6 @@ type FunctionDecl struct { ReturnType string ReturnRef TypeRef Body []Stmt - Suspend bool } type Param struct { diff --git a/internal/lang/coroutines.go b/internal/lang/coroutines.go index be228e7..b2d5c87 100644 --- a/internal/lang/coroutines.go +++ b/internal/lang/coroutines.go @@ -5,17 +5,17 @@ import ( "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 { for _, fn := range program.Functions { - if fn.Suspend || statementsUseCoroutines(fn.Body) { + if statementsUseCoroutines(fn.Body) { return true } } for _, class := range program.Classes { for _, fn := range class.Methods { - if fn.Suspend || statementsUseCoroutines(fn.Body) { + if statementsUseCoroutines(fn.Body) { 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) 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) 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 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 }") diff --git a/internal/lang/coroutines_test.go b/internal/lang/coroutines_test.go index c58848b..6178e9b 100644 --- a/internal/lang/coroutines_test.go +++ b/internal/lang/coroutines_test.go @@ -7,7 +7,7 @@ import ( func TestGenerateStructuredCoroutines(t *testing.T) { prog, err := Parse(`package demo -suspend fun load(): Int { delay(1); return 42 } +fun load(): Int { delay(1); return 42 } fun main() { runBlocking { coroutineScope { @@ -31,16 +31,35 @@ fun main() { } } -func TestSuspendFunctionRequiresScope(t *testing.T) { +func TestInferredCoroutineFunctionRequiresScope(t *testing.T) { prog, err := Parse(`package demo -suspend fun load(): Int { return 1 } +fun load(): Int { delay(1); 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) + out, err := GenerateGo(prog) + if err == nil || !strings.Contains(err.Error(), "contextual function main requires a runBlocking boundary") { + 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) { prog, err := Parse(`package demo fun main() { runBlocking { launch { delay(1) }; panic("failed") } }`) if err != nil { diff --git a/internal/lang/effects.go b/internal/lang/effects.go new file mode 100644 index 0000000..d928d03 --- /dev/null +++ b/internal/lang/effects.go @@ -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) + } +} diff --git a/internal/lang/generate_go.go b/internal/lang/generate_go.go index 16cc9aa..e3c72e6 100644 --- a/internal/lang/generate_go.go +++ b/internal/lang/generate_go.go @@ -251,7 +251,11 @@ func (g *goGenerator) function(fn FunctionDecl) error { g.currentClass = nil g.currentFunc = fn 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" } defer func() { g.currentCoroutineScope = previousScope }() @@ -284,11 +288,7 @@ func (g *goGenerator) interfaceDecl(decl InterfaceDecl) { g.line("type " + decl.Name + " interface {") g.indentLevel++ for _, method := range decl.Methods { - prefix := "" - if method.Suspend { - prefix = "gotlinScope *GotlinCoroutineScope" - } - g.line(method.Name + g.renderGoParamsWithPrefix(method.Params, prefix) + g.renderGoReturnSuffix(method.ReturnType)) + g.line(method.Name + g.renderGoParamsWithPrefix(method.Params, "") + g.renderGoReturnSuffix(method.ReturnType)) } g.indentLevel-- g.line("}") @@ -416,7 +416,7 @@ func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error { g.currentClass = &class g.currentFunc = fn previousScope := g.currentCoroutineScope - if fn.Suspend { + if g.hasCoroutineEffect(fn) { g.currentCoroutineScope = "gotlinScope" } 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 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 { @@ -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 } } - if ident, ok := e.Callee.(IdentExpr); ok { - if fn, found := g.semantic.Functions[ident.Name]; found && fn.Suspend { + if resolvedCall != nil { + if call, ok := resolvedCall.Node.(HIRGotlinCall); ok && call.Target != nil && call.Target.Effects.Has(CoroutineEffect) { 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...) } } - if selector, ok := e.Callee.(SelectorExpr); ok { - if class, found := g.classForType(g.exprType(selector.Receiver)); found { - for _, method := range class.Methods { - if method.Name == selector.Name && method.Suspend { - if g.currentCoroutineScope == "" { - return "", fmt.Errorf("suspend method %s requires a coroutine scope", selector.Name) - } - args = append([]string{g.currentCoroutineScope}, args...) - break - } - } - } - } callee, err := g.expr(e.Callee, "") if err != nil { return "", err @@ -2491,12 +2483,20 @@ func (g *goGenerator) cloneScopes() []map[string]bool { func (g *goGenerator) renderGoFunctionParams(function FunctionDecl) string { prefix := "" - if function.Suspend { + if g.hasCoroutineEffect(function) { prefix = "gotlinScope *GotlinCoroutineScope" } 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 { if len(params) == 0 { return "" diff --git a/internal/lang/hir.go b/internal/lang/hir.go index 245df71..0c8a288 100644 --- a/internal/lang/hir.go +++ b/internal/lang/hir.go @@ -600,6 +600,11 @@ func (resolver *semanticResolver) meaning(expr Expr, scope *Scope) (ExprMeaning, if root, ok := selectorRootAlias(selector); ok && resolver.program.Imports[root] { 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) if !isUnknownType(resolver.program.goMethodType(receiverType, selector.Name)) { return GoCallExpr, nil @@ -755,7 +760,7 @@ var semanticBuiltins = map[string]bool{ "panic": true, "recover": true, "string": true, "int": true, "float64": true, "bool": true, "sql": true, "set": true, "now": true, "Result": true, "ByteSlice": 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, } diff --git a/internal/lang/parser.go b/internal/lang/parser.go index 91798c9..bc7f9e0 100644 --- a/internal/lang/parser.go +++ b/internal/lang/parser.go @@ -138,7 +138,7 @@ func (p *parser) parseProgram() (*Program, error) { default: return nil, fmt.Errorf("unsupported annotation %q", annotation.lexeme) } - case p.check(tokenFun) || p.check(tokenSuspend): + case p.check(tokenFun): fn, err := p.parseFunction() if err != nil { return nil, err @@ -325,7 +325,7 @@ func (p *parser) parseClass() (ClassDecl, error) { var methods []FunctionDecl for !p.check(tokenRBrace) && !p.check(tokenEOF) { p.match(tokenOverride) - if !p.check(tokenFun) && !p.check(tokenSuspend) { + if !p.check(tokenFun) { tok := p.peek() 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, 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 } @@ -524,7 +522,6 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) { TypeParams: typeParams, Params: params, ReturnType: returnType, - Suspend: suspend, }, nil } diff --git a/internal/lang/symbols.go b/internal/lang/symbols.go index 9df95df..be312cf 100644 --- a/internal/lang/symbols.go +++ b/internal/lang/symbols.go @@ -21,6 +21,7 @@ type Symbol struct { Type Type Mutable bool Decl any + Effects Effect } type Scope struct { @@ -56,17 +57,18 @@ type ClassSymbol struct { } type SemanticProgram struct { - Syntax *Program - Global *Scope - Classes map[string]ClassDecl - ClassInfo map[string]*ClassSymbol - Functions map[string]FunctionDecl - Enums map[string]EnumDecl - Imports map[string]bool - GoPackages map[string]*gotypes.Package - HIR *HIRProgram - Mappings *mappingState - Diagnostics []SemanticDiagnostic + Syntax *Program + Global *Scope + Classes map[string]ClassDecl + ClassInfo map[string]*ClassSymbol + Functions map[string]FunctionDecl + Enums map[string]EnumDecl + Imports map[string]bool + GoPackages map[string]*gotypes.Package + HIR *HIRProgram + Mappings *mappingState + Diagnostics []SemanticDiagnostic + FunctionEffects map[string]Effect } 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) { semantic := &SemanticProgram{ - Syntax: program, - Global: NewScope(nil), - Classes: map[string]ClassDecl{}, - ClassInfo: map[string]*ClassSymbol{}, - Functions: map[string]FunctionDecl{}, - Enums: map[string]EnumDecl{}, - Imports: map[string]bool{}, - GoPackages: map[string]*gotypes.Package{}, - Mappings: &mappingState{functions: map[string]string{}}, + Syntax: program, + Global: NewScope(nil), + Classes: map[string]ClassDecl{}, + ClassInfo: map[string]*ClassSymbol{}, + Functions: map[string]FunctionDecl{}, + Enums: map[string]EnumDecl{}, + Imports: map[string]bool{}, + GoPackages: map[string]*gotypes.Package{}, + Mappings: &mappingState{functions: map[string]string{}}, + FunctionEffects: map[string]Effect{}, } programs := append([]*Program{program}, additional...) for _, source := range programs { @@ -198,6 +201,7 @@ func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, if err := resolver.resolve(); err != nil { return nil, err } + inferCoroutineEffects(semantic) return semantic, nil } diff --git a/internal/lang/token.go b/internal/lang/token.go index e5b86a2..3b95fd1 100644 --- a/internal/lang/token.go +++ b/internal/lang/token.go @@ -19,7 +19,6 @@ const ( tokenEnum tokenKind = "ENUM" tokenMatch tokenKind = "MATCH" tokenFun tokenKind = "FUN" - tokenSuspend tokenKind = "SUSPEND" tokenOverride tokenKind = "OVERRIDE" tokenPrivate tokenKind = "PRIVATE" tokenVal tokenKind = "VAL" @@ -70,7 +69,6 @@ const ( var keywords = map[string]tokenKind{ "fun": tokenFun, - "suspend": tokenSuspend, "import": tokenImport, "package": tokenPackage, "class": tokenClass, diff --git a/internal/lang/type_checker.go b/internal/lang/type_checker.go index a330d3c..6110263 100644 --- a/internal/lang/type_checker.go +++ b/internal/lang/type_checker.go @@ -125,6 +125,8 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) if len(value.TypeArgs) == 1 { 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 mapping, ok := semantic.TypeOf(value.Args[0], environment).(GenericType); ok && (mapping.Base.String() == "Map" || mapping.Base.String() == "MutableMap") && len(mapping.Args) == 2 { diff --git a/internal/lang/types.go b/internal/lang/types.go index 5883fbc..2118241 100644 --- a/internal/lang/types.go +++ b/internal/lang/types.go @@ -57,6 +57,7 @@ type FunctionType struct { TypeParams []string Params []Type Result Type + Effects Effect } func (FunctionType) typeNode() {} @@ -200,7 +201,7 @@ func resolveTypeParameters(typ Type, params map[string]bool) Type { for index, param := range value.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: args := make([]Type, len(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 { 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: return typ } diff --git a/tools/vscode-gotlin/scripts/validate.js b/tools/vscode-gotlin/scripts/validate.js index 3994241..7fc1149 100644 --- a/tools/vscode-gotlin/scripts/validate.js +++ b/tools/vscode-gotlin/scripts/validate.js @@ -42,7 +42,7 @@ assert(language.folding?.markers?.start && language.indentationRules?.increaseIn const grammarSource = JSON.stringify(grammar); 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", "throw", "enum", "match", "package", "import", "jsonNaming", "embed", "table", "column", "id", "generated", "Int", "Long", "String", "Boolean", "Unit", "Double", "Float", "Any", diff --git a/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json b/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json index 5282a34..8c52ff5 100644 --- a/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json +++ b/tools/vscode-gotlin/syntaxes/gotlin.tmLanguage.json @@ -254,7 +254,7 @@ "patterns": [ { "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",