Infer coroutine effects automatically
This commit is contained in:
parent
bac1183593
commit
5b30e49486
15 changed files with 339 additions and 103 deletions
14
README.md
14
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
|
- 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1610,13 +1610,14 @@ var builtinDetails = map[string]string{
|
||||||
"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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 }")
|
||||||
|
|
|
||||||
|
|
@ -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
155
internal/lang/effects.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -67,6 +68,7 @@ type SemanticProgram struct {
|
||||||
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) {
|
||||||
|
|
@ -100,6 +102,7 @@ func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram,
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue