diff --git a/README.md b/README.md index d180355..1c4fc25 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,21 @@ 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. +For conventional Go APIs whose first parameter is `context.Context`, Gotlin +normally injects that ambient context automatically when the argument is +omitted: + +```kotlin +fun command(): *exec.Cmd { + return exec.commandContext("date") +} +``` + +The function becomes contextually effectful and lowers to +`exec.CommandContext(gotlinScope.Context(), "date")`. Passing an explicit +context remains supported and suppresses injection, which is important for HTTP +request contexts and deliberately detached work. + ```kotlin fun load(): Int { delay(10) diff --git a/internal/lang/coroutines_test.go b/internal/lang/coroutines_test.go index 6178e9b..13b3fc5 100644 --- a/internal/lang/coroutines_test.go +++ b/internal/lang/coroutines_test.go @@ -116,6 +116,48 @@ fun main() { runBlocking { useContext() } }`) } } +func TestGoContextIsInjectedFromAmbientScope(t *testing.T) { + program, err := Parse(`package demo +import exec os.exec +fun command(): *exec.Cmd { return exec.commandContext("date") } +fun main() { runBlocking { println(command()) } }`) + if err != nil { + t.Fatal(err) + } + output, err := GenerateGo(program) + if err != nil { + t.Fatal(err) + } + for _, expected := range []string{"func command(gotlinScope *GotlinCoroutineScope) *exec.Cmd", `exec.CommandContext(gotlinScope.Context(), "date")`, "command(gotlinScope)"} { + if !strings.Contains(string(output), expected) { + t.Fatalf("missing %q:\n%s", expected, output) + } + } + call := program.Functions[0].Body[0].(ReturnStmt).Value.(CallExpr) + goCall, ok := call.Meta.Semantic.Node.(HIRGoCall) + if !ok || !goCall.InjectContext || !goCall.Variadic { + t.Fatalf("Go call context metadata = %#v", call.Meta.Semantic.Node) + } +} + +func TestExplicitGoContextPreventsAmbientInjection(t *testing.T) { + program, err := Parse(`package demo +import context +import exec os.exec +fun command(ctx: context.Context): *exec.Cmd { return exec.commandContext(ctx, "date") }`) + if err != nil { + t.Fatal(err) + } + output, err := GenerateGo(program) + if err != nil { + t.Fatal(err) + } + code := string(output) + if strings.Contains(code, "func command(gotlinScope") || !strings.Contains(code, `exec.CommandContext(ctx, "date")`) { + t.Fatalf("explicit context was not preserved:\n%s", code) + } +} + 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 index d928d03..3003c6c 100644 --- a/internal/lang/effects.go +++ b/internal/lang/effects.go @@ -120,6 +120,9 @@ func collectExpressionEffects(expression Expr, node *effectNode) { if call, ok := semantic.Node.(HIRGotlinCall); ok && call.Target != nil { node.callees = append(node.callees, call.Target) } + if call, ok := semantic.Node.(HIRGoCall); ok && call.InjectContext { + node.direct |= CoroutineEffect + } } collectExpressionEffects(value.Callee, node) for _, argument := range value.Args { diff --git a/internal/lang/generate_go.go b/internal/lang/generate_go.go index afb23cf..053e98c 100644 --- a/internal/lang/generate_go.go +++ b/internal/lang/generate_go.go @@ -1217,6 +1217,12 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { } args = append([]string{g.currentCoroutineScope}, args...) } + if call, ok := resolvedCall.Node.(HIRGoCall); ok && call.InjectContext { + if g.currentCoroutineScope == "" { + return "", fmt.Errorf("Go function requires an ambient coroutine context") + } + args = append([]string{g.currentCoroutineScope + ".Context()"}, args...) + } } callee, err := g.expr(e.Callee, "") if err != nil { diff --git a/internal/lang/go_types.go b/internal/lang/go_types.go index 9a14c37..1e66431 100644 --- a/internal/lang/go_types.go +++ b/internal/lang/go_types.go @@ -108,13 +108,44 @@ func (semantic *SemanticProgram) goFieldType(receiver Type, name string) Type { return UnknownType{} } +func (semantic *SemanticProgram) goCallSignature(call CallExpr) (FunctionType, bool) { + selector, ok := call.Callee.(SelectorExpr) + if !ok { + return FunctionType{}, false + } + if receiver, ok := selector.Receiver.(IdentExpr); ok && semantic.Imports[receiver.Name] && semantic.Packages[receiver.Name] == nil { + function, ok := semantic.goSelectorType(receiver.Name, selector.Name).(FunctionType) + return function, ok + } + receiverType := ResolvedType(selector.Receiver) + function, ok := semantic.goMethodType(receiverType, selector.Name).(FunctionType) + return function, ok +} + +func shouldInjectCoroutineContext(call CallExpr, signature FunctionType) bool { + if len(signature.Params) == 0 || signature.Params[0].String() != "context.Context" { + return false + } + if len(call.Args) > 0 && ResolvedType(call.Args[0]).String() == "context.Context" { + return false + } + if signature.Variadic { + minimumWithoutContext := len(signature.Params) - 2 + if minimumWithoutContext < 0 { + minimumWithoutContext = 0 + } + return len(call.Args) >= minimumWithoutContext + } + return len(call.Args) == len(signature.Params)-1 +} + func semanticTypeFromGoSignature(signature *gotypes.Signature) Type { params := make([]Type, signature.Params().Len()) for index := range params { params[index] = semanticTypeFromGo(signature.Params().At(index).Type()) } result := semanticTypeFromGoResults(signature.Results()) - return FunctionType{Params: params, Result: result} + return FunctionType{Params: params, Result: result, Variadic: signature.Variadic()} } func semanticTypeFromGoResults(results *gotypes.Tuple) Type { diff --git a/internal/lang/hir.go b/internal/lang/hir.go index 2505e74..7d7044a 100644 --- a/internal/lang/hir.go +++ b/internal/lang/hir.go @@ -46,8 +46,11 @@ type HIRReference struct{ Target *Symbol } func (HIRReference) hirNode() {} type HIRGoCall struct { - Callee *HIRExpr - Result Type + Callee *HIRExpr + Result Type + Params []Type + Variadic bool + InjectContext bool } func (HIRGoCall) hirNode() {} @@ -498,7 +501,8 @@ func (resolver *semanticResolver) hirNode(expr Expr, semantic *HIRExpr) HIRNode return HIRReference{Target: semantic.Symbol} case GoCallExpr: if call, ok := expr.(CallExpr); ok { - return HIRGoCall{Callee: exprMeta(call.Callee), Result: semantic.Type} + signature, _ := resolver.program.goCallSignature(call) + return HIRGoCall{Callee: exprMeta(call.Callee), Result: semantic.Type, Params: signature.Params, Variadic: signature.Variadic, InjectContext: shouldInjectCoroutineContext(call, signature)} } case GotlinCallExpr, MethodCallExpr: return HIRGotlinCall{Target: semantic.Symbol, Result: semantic.Type} diff --git a/internal/lang/sql_test.go b/internal/lang/sql_test.go index 8e21151..8c083a5 100644 --- a/internal/lang/sql_test.go +++ b/internal/lang/sql_test.go @@ -79,7 +79,8 @@ fun accountQuery(): GotlinSQLQuery { func TestGeneratedQueryCanBePassedToVariadicPGXCall(t *testing.T) { code := compileSQL(t, accountRowSource+` import pgxpool "github.com/jackc/pgx/v5/pgxpool" -fun execute(pool: *pgxpool.Pool, ctx: Context, customerId: String) { +import context +fun execute(pool: *pgxpool.Pool, ctx: context.Context, customerId: String) { val query = sql.from().where { it.customerId == customerId }.build() pool.query(ctx, query.sql, *query.args) } diff --git a/internal/lang/types.go b/internal/lang/types.go index 4998ff8..6d8642f 100644 --- a/internal/lang/types.go +++ b/internal/lang/types.go @@ -66,6 +66,7 @@ type FunctionType struct { Params []Type Result Type Effects Effect + Variadic bool } func (FunctionType) typeNode() {} @@ -175,7 +176,7 @@ func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type { for i, param := range value.Params { params[i] = resolveClassTypes(param, classes) } - return FunctionType{Params: params, Result: resolveClassTypes(value.Result, classes)} + return FunctionType{TypeParams: value.TypeParams, Params: params, Result: resolveClassTypes(value.Result, classes), Effects: value.Effects, Variadic: value.Variadic} case GenericType: args := make([]Type, len(value.Args)) for i, arg := range value.Args { @@ -220,7 +221,7 @@ func resolveImportedTypes(typ Type, packages map[string]*PackageSymbol) Type { for index, param := range value.Params { params[index] = resolveImportedTypes(param, packages) } - return FunctionType{TypeParams: value.TypeParams, Params: params, Result: resolveImportedTypes(value.Result, packages), Effects: value.Effects} + return FunctionType{TypeParams: value.TypeParams, Params: params, Result: resolveImportedTypes(value.Result, packages), Effects: value.Effects, Variadic: value.Variadic} default: return typ } @@ -242,7 +243,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), Effects: value.Effects} + return FunctionType{TypeParams: value.TypeParams, Params: resolved, Result: resolveTypeParameters(value.Result, params), Effects: value.Effects, Variadic: value.Variadic} case GenericType: args := make([]Type, len(value.Args)) for index, arg := range value.Args { @@ -303,7 +304,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), Effects: value.Effects} + return FunctionType{TypeParams: value.TypeParams, Params: params, Result: substituteType(value.Result, bindings), Effects: value.Effects, Variadic: value.Variadic} default: return typ }