Inject ambient context into Go calls
This commit is contained in:
parent
acb42a5702
commit
5085ae51aa
8 changed files with 112 additions and 9 deletions
15
README.md
15
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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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<AccountRow>().where { it.customerId == customerId }.build()
|
||||
pool.query(ctx, query.sql, *query.args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue