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

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