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
|
`context.Context`; it returns the ambient scope context without exposing it in
|
||||||
the Gotlin function signature.
|
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
|
```kotlin
|
||||||
fun load(): Int {
|
fun load(): Int {
|
||||||
delay(10)
|
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) {
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,9 @@ func collectExpressionEffects(expression Expr, node *effectNode) {
|
||||||
if call, ok := semantic.Node.(HIRGotlinCall); ok && call.Target != nil {
|
if call, ok := semantic.Node.(HIRGotlinCall); ok && call.Target != nil {
|
||||||
node.callees = append(node.callees, call.Target)
|
node.callees = append(node.callees, call.Target)
|
||||||
}
|
}
|
||||||
|
if call, ok := semantic.Node.(HIRGoCall); ok && call.InjectContext {
|
||||||
|
node.direct |= CoroutineEffect
|
||||||
|
}
|
||||||
}
|
}
|
||||||
collectExpressionEffects(value.Callee, node)
|
collectExpressionEffects(value.Callee, node)
|
||||||
for _, argument := range value.Args {
|
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...)
|
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, "")
|
callee, err := g.expr(e.Callee, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -108,13 +108,44 @@ func (semantic *SemanticProgram) goFieldType(receiver Type, name string) Type {
|
||||||
return UnknownType{}
|
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 {
|
func semanticTypeFromGoSignature(signature *gotypes.Signature) Type {
|
||||||
params := make([]Type, signature.Params().Len())
|
params := make([]Type, signature.Params().Len())
|
||||||
for index := range params {
|
for index := range params {
|
||||||
params[index] = semanticTypeFromGo(signature.Params().At(index).Type())
|
params[index] = semanticTypeFromGo(signature.Params().At(index).Type())
|
||||||
}
|
}
|
||||||
result := semanticTypeFromGoResults(signature.Results())
|
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 {
|
func semanticTypeFromGoResults(results *gotypes.Tuple) Type {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ func (HIRReference) hirNode() {}
|
||||||
type HIRGoCall struct {
|
type HIRGoCall struct {
|
||||||
Callee *HIRExpr
|
Callee *HIRExpr
|
||||||
Result Type
|
Result Type
|
||||||
|
Params []Type
|
||||||
|
Variadic bool
|
||||||
|
InjectContext bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (HIRGoCall) hirNode() {}
|
func (HIRGoCall) hirNode() {}
|
||||||
|
|
@ -498,7 +501,8 @@ func (resolver *semanticResolver) hirNode(expr Expr, semantic *HIRExpr) HIRNode
|
||||||
return HIRReference{Target: semantic.Symbol}
|
return HIRReference{Target: semantic.Symbol}
|
||||||
case GoCallExpr:
|
case GoCallExpr:
|
||||||
if call, ok := expr.(CallExpr); ok {
|
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:
|
case GotlinCallExpr, MethodCallExpr:
|
||||||
return HIRGotlinCall{Target: semantic.Symbol, Result: semantic.Type}
|
return HIRGotlinCall{Target: semantic.Symbol, Result: semantic.Type}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ fun accountQuery(): GotlinSQLQuery {
|
||||||
func TestGeneratedQueryCanBePassedToVariadicPGXCall(t *testing.T) {
|
func TestGeneratedQueryCanBePassedToVariadicPGXCall(t *testing.T) {
|
||||||
code := compileSQL(t, accountRowSource+`
|
code := compileSQL(t, accountRowSource+`
|
||||||
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
|
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()
|
val query = sql.from<AccountRow>().where { it.customerId == customerId }.build()
|
||||||
pool.query(ctx, query.sql, *query.args)
|
pool.query(ctx, query.sql, *query.args)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ type FunctionType struct {
|
||||||
Params []Type
|
Params []Type
|
||||||
Result Type
|
Result Type
|
||||||
Effects Effect
|
Effects Effect
|
||||||
|
Variadic bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (FunctionType) typeNode() {}
|
func (FunctionType) typeNode() {}
|
||||||
|
|
@ -175,7 +176,7 @@ func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type {
|
||||||
for i, param := range value.Params {
|
for i, param := range value.Params {
|
||||||
params[i] = resolveClassTypes(param, classes)
|
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:
|
case GenericType:
|
||||||
args := make([]Type, len(value.Args))
|
args := make([]Type, len(value.Args))
|
||||||
for i, arg := range 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 {
|
for index, param := range value.Params {
|
||||||
params[index] = resolveImportedTypes(param, packages)
|
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:
|
default:
|
||||||
return typ
|
return typ
|
||||||
}
|
}
|
||||||
|
|
@ -242,7 +243,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), Effects: value.Effects}
|
return FunctionType{TypeParams: value.TypeParams, Params: resolved, Result: resolveTypeParameters(value.Result, params), Effects: value.Effects, Variadic: value.Variadic}
|
||||||
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 {
|
||||||
|
|
@ -303,7 +304,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), Effects: value.Effects}
|
return FunctionType{TypeParams: value.TypeParams, Params: params, Result: substituteType(value.Result, bindings), Effects: value.Effects, Variadic: value.Variadic}
|
||||||
default:
|
default:
|
||||||
return typ
|
return typ
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue