From 773c34f3f4cfb1586499b42e1307bb1e2e22e52c Mon Sep 17 00:00:00 2001 From: pavel Date: Thu, 27 Aug 2026 23:05:49 +0200 Subject: [PATCH] Add expression functions with context boundaries --- README.md | 13 +++ cmd/gotlin-lsp/main.go | 10 ++- examples/http_context.gt | 12 +++ internal/lang/ast.go | 26 +++--- internal/lang/coroutines.go | 8 +- internal/lang/effects.go | 18 +++- internal/lang/expression_function_test.go | 86 +++++++++++++++++++ internal/lang/generate_go.go | 86 ++++++++++++++++--- internal/lang/hir.go | 85 ++++++++++++++++-- internal/lang/metadata.go | 24 +++++- internal/lang/parser.go | 37 +++++--- internal/lang/semantics.go | 3 + internal/lang/sql.go | 6 ++ internal/lang/symbols.go | 10 +++ internal/lang/type_checker.go | 2 + internal/lang/type_refs.go | 5 ++ tools/vscode-gotlin/scripts/validate.js | 2 +- .../snippets/gotlin.code-snippets | 10 +++ 18 files changed, 391 insertions(+), 52 deletions(-) create mode 100644 examples/http_context.gt create mode 100644 internal/lang/expression_function_test.go diff --git a/README.md b/README.md index 1c4fc25..8e86c32 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,19 @@ 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. +HTTP handlers can establish a request-scoped ambient context with an +expression-bodied function: + +```kotlin +fun handle(request: *http.Request) = withContext(request.context()) { + service.processRequest() +} +``` + +`withContext` is a structured boundary. It waits for children, propagates +failures, and, when nested, combines cancellation from the parent coroutine and +the supplied Go context. + For conventional Go APIs whose first parameter is `context.Context`, Gotlin normally injects that ambient context automatically when the argument is omitted: diff --git a/cmd/gotlin-lsp/main.go b/cmd/gotlin-lsp/main.go index 6fcd78a..152de49 100644 --- a/cmd/gotlin-lsp/main.go +++ b/cmd/gotlin-lsp/main.go @@ -1034,7 +1034,14 @@ func renderFunctionSignature(fn lang.FunctionDecl) string { if len(fn.TypeParams) > 0 { name += "<" + strings.Join(fn.TypeParams, ", ") + ">" } - return renderFunctionSignatureFromParts(name, fn.Params, fn.ReturnType) + returnType := fn.ReturnType + if fn.InferReturn && fn.ExpressionBody != nil { + resolved := lang.ResolvedType(fn.ExpressionBody) + if resolved.String() != "" { + returnType = resolved.String() + } + } + return renderFunctionSignatureFromParts(name, fn.Params, returnType) } func renderFunctionSignatureFromParts(name string, fnParams []lang.Param, returnType string) string { @@ -1640,6 +1647,7 @@ var builtinDetails = map[string]string{ "set": "fun set(target: Any, value: Any): Unit", "now": "fun now(): time.Time", "runBlocking": "fun runBlocking(block: () -> Unit): Unit", + "withContext": "fun withContext(ctx: context.Context, block: () -> Unit): Unit", "coroutineScope": "contextual fun coroutineScope(block: () -> Unit): Unit", "launch": "contextual fun launch(block: () -> Unit): Unit", "async": "contextual fun async(block: () -> T): Deferred", diff --git a/examples/http_context.gt b/examples/http_context.gt new file mode 100644 index 0000000..634f4e2 --- /dev/null +++ b/examples/http_context.gt @@ -0,0 +1,12 @@ +package main + +import fmt +import http net.http + +fun handle(request: *http.Request) = withContext(request.context()) { + fmt.println(coroutineContext()) +} + +fun main() { + println("HTTP handlers can establish their request context with withContext") +} diff --git a/internal/lang/ast.go b/internal/lang/ast.go index 0f4e7c8..3d2df48 100644 --- a/internal/lang/ast.go +++ b/internal/lang/ast.go @@ -60,20 +60,24 @@ type FieldDecl struct { } type FunctionSignature struct { - Name string - TypeParams []string - Params []Param - ReturnType string - ReturnRef TypeRef + Name string + TypeParams []string + Params []Param + ReturnType string + ReturnRef TypeRef + ReturnExplicit bool } type FunctionDecl struct { - Name string - TypeParams []string - Params []Param - ReturnType string - ReturnRef TypeRef - Body []Stmt + Name string + TypeParams []string + Params []Param + ReturnType string + ReturnRef TypeRef + ReturnExplicit bool + Body []Stmt + ExpressionBody Expr + InferReturn bool } type Param struct { diff --git a/internal/lang/coroutines.go b/internal/lang/coroutines.go index b2d5c87..57eccb5 100644 --- a/internal/lang/coroutines.go +++ b/internal/lang/coroutines.go @@ -5,17 +5,17 @@ import ( "strings" ) -var coroutineBuiltins = map[string]bool{"runBlocking": true, "coroutineScope": true, "launch": true, "async": true, "delay": true, "withTimeout": true, "isActive": true, "coroutineContext": true} +var coroutineBuiltins = map[string]bool{"runBlocking": true, "withContext": true, "coroutineScope": true, "launch": true, "async": true, "delay": true, "withTimeout": true, "isActive": true, "coroutineContext": true} func programUsesCoroutines(program *Program) bool { for _, fn := range program.Functions { - if statementsUseCoroutines(fn.Body) { + if statementsUseCoroutines(fn.Body) || (fn.ExpressionBody != nil && expressionUsesCoroutines(fn.ExpressionBody)) { return true } } for _, class := range program.Classes { for _, fn := range class.Methods { - if statementsUseCoroutines(fn.Body) { + if statementsUseCoroutines(fn.Body) || (fn.ExpressionBody != nil && expressionUsesCoroutines(fn.ExpressionBody)) { return true } } @@ -96,7 +96,9 @@ func (g *goGenerator) emitCoroutineSupport() { 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) WithContext(ctx context.Context, block func(*GotlinCoroutineScope)) { merged,cancel:=context.WithCancel(ctx); stop:=context.AfterFunc(scope.ctx,cancel); defer stop(); defer cancel(); child:=gotlinNewCoroutineScope(merged); 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 gotlinRunWithContext(ctx context.Context, block func(*GotlinCoroutineScope)) { scope:=gotlinNewCoroutineScope(ctx); 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("func gotlinAsync[T any](scope *GotlinCoroutineScope, block func(*GotlinCoroutineScope) T) *GotlinDeferred[T] { deferred:=&GotlinDeferred[T]{done:make(chan struct{})}; scope.Launch(func(child *GotlinCoroutineScope){defer close(deferred.done);defer func(){if value:=recover();value!=nil{deferred.failure=value;scope.fail(value)}}();deferred.value=block(child)}); return deferred }") g.line("func (deferred *GotlinDeferred[T]) await() T { <-deferred.done; if deferred.failure!=nil{panic(deferred.failure)}; return deferred.value }") diff --git a/internal/lang/effects.go b/internal/lang/effects.go index 3003c6c..5bf4d95 100644 --- a/internal/lang/effects.go +++ b/internal/lang/effects.go @@ -22,7 +22,11 @@ func inferCoroutineEffects(semantic *SemanticProgram) { decl := &semantic.Syntax.Functions[index] symbol, _ := semantic.Global.Lookup(decl.Name) node := &effectNode{key: decl.Name, symbol: symbol} - collectFunctionEffects(decl.Body, node) + if decl.ExpressionBody != nil { + collectExpressionEffects(decl.ExpressionBody, node) + } else { + collectFunctionEffects(decl.Body, node) + } nodes = append(nodes, node) } for classIndex := range semantic.Syntax.Classes { @@ -31,7 +35,11 @@ func inferCoroutineEffects(semantic *SemanticProgram) { 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) + if method.ExpressionBody != nil { + collectExpressionEffects(method.ExpressionBody, node) + } else { + collectFunctionEffects(method.Body, node) + } nodes = append(nodes, node) } } @@ -112,6 +120,12 @@ func collectExpressionEffects(expression Expr, node *effectNode) { if ident.Name == "runBlocking" { return } + if ident.Name == "withContext" { + if len(value.Args) > 0 { + collectExpressionEffects(value.Args[0], node) + } + return + } if coroutineBuiltins[ident.Name] { node.direct |= CoroutineEffect } diff --git a/internal/lang/expression_function_test.go b/internal/lang/expression_function_test.go new file mode 100644 index 0000000..0ed4fa3 --- /dev/null +++ b/internal/lang/expression_function_test.go @@ -0,0 +1,86 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestExpressionBodiedFunctionInfersReturnType(t *testing.T) { + program, err := Parse(`package demo +fun twice(value: Int) = value * 2 +fun text() = "value"`) + if err != nil { + t.Fatal(err) + } + output, err := GenerateGo(program) + if err != nil { + t.Fatal(err) + } + for _, expected := range []string{"func twice(value int) int", "return value * 2", "func text() string", `return "value"`} { + if !strings.Contains(string(output), expected) { + t.Fatalf("missing %q:\n%s", expected, output) + } + } + if got := ResolvedType(program.Functions[0].ExpressionBody).String(); got != "Int" { + t.Fatalf("return type = %s", got) + } +} + +func TestExpressionBodyInferenceReachesFixedPoint(t *testing.T) { + program, err := Parse(`package demo +fun first() = second() +fun second() = third() +fun third() = 42`) + if err != nil { + t.Fatal(err) + } + semantic, err := Analyze(program) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{"first", "second", "third"} { + symbol, _ := semantic.Global.Lookup(name) + if got := symbol.Type.(FunctionType).Result.String(); got != "Int" { + t.Fatalf("%s result = %s", name, got) + } + } +} + +func TestWithContextExpressionBodyEstablishesBoundary(t *testing.T) { + program, err := Parse(`package demo +import http net.http +import exec os.exec +fun work() { exec.commandContext("date") } +fun handle(request: *http.Request) = withContext(request.context()) { + work() +}`) + if err != nil { + t.Fatal(err) + } + semantic, err := Analyze(program) + if err != nil { + t.Fatal(err) + } + if !semantic.FunctionEffects["work"].Has(CoroutineEffect) { + t.Fatal("work should require ambient context") + } + if semantic.FunctionEffects["handle"].Has(CoroutineEffect) { + t.Fatal("withContext should be an effect boundary") + } + output, err := GenerateGo(program) + if err != nil { + t.Fatal(err) + } + code := string(output) + for _, expected := range []string{ + "func work(gotlinScope *GotlinCoroutineScope)", + `exec.CommandContext(gotlinScope.Context(), "date")`, + "func handle(request *http.Request)", + "gotlinRunWithContext(request.Context()", + "work(gotlinScope)", + } { + if !strings.Contains(code, expected) { + t.Fatalf("missing %q:\n%s", expected, code) + } + } +} diff --git a/internal/lang/generate_go.go b/internal/lang/generate_go.go index 053e98c..066ac22 100644 --- a/internal/lang/generate_go.go +++ b/internal/lang/generate_go.go @@ -84,7 +84,7 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { g.line("") for _, fn := range program.Functions { - if usesPrintln(fn.Body) { + if usesPrintln(fn.Body) || (fn.ExpressionBody != nil && exprUsesPrintln(fn.ExpressionBody)) { g.needsFmt = true break } @@ -92,7 +92,7 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { if !g.needsFmt { for _, class := range program.Classes { for _, method := range class.Methods { - if usesPrintln(method.Body) { + if usesPrintln(method.Body) || (method.ExpressionBody != nil && exprUsesPrintln(method.ExpressionBody)) { g.needsFmt = true break } @@ -104,7 +104,7 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { } if !g.needsRunCatch { for _, fn := range program.Functions { - if usesRunCatching(fn.Body) { + if usesRunCatching(fn.Body) || (fn.ExpressionBody != nil && exprUsesRunCatching(fn.ExpressionBody)) { g.needsRunCatch = true break } @@ -112,7 +112,7 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { } if !g.needsGoUnwrap { for _, fn := range program.Functions { - if usesGoUnwrap(fn.Body) { + if usesGoUnwrap(fn.Body) || fn.ExpressionBody != nil { g.needsGoUnwrap = true break } @@ -121,7 +121,7 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { if !g.needsGoUnwrap { for _, class := range program.Classes { for _, method := range class.Methods { - if usesGoUnwrap(method.Body) { + if usesGoUnwrap(method.Body) || method.ExpressionBody != nil { g.needsGoUnwrap = true break } @@ -134,7 +134,7 @@ func (g *goGenerator) program(program *Program, packageOverride string) error { if !g.needsRunCatch { for _, class := range program.Classes { for _, method := range class.Methods { - if usesRunCatching(method.Body) { + if usesRunCatching(method.Body) || (method.ExpressionBody != nil && exprUsesRunCatching(method.ExpressionBody)) { g.needsRunCatch = true break } @@ -277,13 +277,24 @@ func (g *goGenerator) function(fn FunctionDecl) error { g.write(fn.Name) g.write(renderGoTypeParameters(fn.TypeParams)) g.write(g.renderGoFunctionParams(fn)) - if ret := g.goReturnType(fn.ReturnType); ret != "" { + returnType := g.functionReturnType(fn) + if ret := g.goReturnType(returnType); ret != "" { g.write(" ") g.write(ret) } g.write(" {\n") g.indentLevel++ - if err := g.block(fn.Body); err != nil { + if fn.ExpressionBody != nil { + value, err := g.expr(fn.ExpressionBody, returnType) + if err != nil { + return fmt.Errorf("function %s: %w", fn.Name, err) + } + if g.goReturnType(returnType) == "" { + g.line(value) + } else { + g.line("return " + value) + } + } else if err := g.block(fn.Body); err != nil { return fmt.Errorf("function %s: %w", fn.Name, err) } g.indentLevel-- @@ -443,13 +454,24 @@ func (g *goGenerator) method(class ClassDecl, fn FunctionDecl) error { g.write(") ") g.write(fn.Name) g.write(g.renderGoFunctionParams(fn)) - if ret := g.goReturnType(fn.ReturnType); ret != "" { + returnType := g.functionReturnType(fn) + if ret := g.goReturnType(returnType); ret != "" { g.write(" ") g.write(ret) } g.write(" {\n") g.indentLevel++ - if err := g.block(fn.Body); err != nil { + if fn.ExpressionBody != nil { + value, err := g.expr(fn.ExpressionBody, returnType) + if err != nil { + return fmt.Errorf("method %s.%s: %w", class.Name, fn.Name, err) + } + if g.goReturnType(returnType) == "" { + g.line(value) + } else { + g.line("return " + value) + } + } else if err := g.block(fn.Body); err != nil { return fmt.Errorf("method %s.%s: %w", class.Name, fn.Name, err) } g.indentLevel-- @@ -898,6 +920,26 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { return "", err } return "gotlinRunBlocking(" + block + ")", nil + case "withContext": + if len(e.Args) != 2 { + return "", fmt.Errorf("withContext expects a Go context and a lambda") + } + ctx, err := g.expr(e.Args[0], "context.Context") + if err != nil { + return "", err + } + lambda, ok := e.Args[1].(LambdaExpr) + if !ok { + return "", fmt.Errorf("withContext expects a lambda") + } + block, err := g.coroutineLambda(lambda, "Unit") + if err != nil { + return "", err + } + if g.currentCoroutineScope == "" { + return "gotlinRunWithContext(" + ctx + ", " + block + ")", nil + } + return g.currentCoroutineScope + ".WithContext(" + ctx + ", " + block + ")", nil case "coroutineScope", "launch": if g.currentCoroutineScope == "" { return "", fmt.Errorf("%s requires a coroutine scope", ident.Name) @@ -2521,6 +2563,24 @@ func (g *goGenerator) hasCoroutineEffect(function FunctionDecl) bool { return g.semantic.FunctionEffects[key].Has(CoroutineEffect) } +func (g *goGenerator) functionReturnType(function FunctionDecl) string { + if !function.InferReturn { + return function.ReturnType + } + var symbol *Symbol + if g.currentClass != nil { + symbol = g.semantic.ClassInfo[g.currentClass.Name].Method(function.Name) + } else { + symbol, _ = g.semantic.Global.Lookup(function.Name) + } + if symbol != nil { + if signature, ok := symbol.Type.(FunctionType); ok { + return signature.Result.String() + } + } + return function.ReturnType +} + func renderGoTypeParameters(params []string) string { if len(params) == 0 { return "" @@ -2855,6 +2915,9 @@ func usedImportAliases(program *Program) map[string]bool { markType(param.Type) } markType(method.ReturnType) + if method.ExpressionBody != nil { + walkExpr(method.ExpressionBody) + } for _, stmt := range method.Body { walkStmt(stmt) } @@ -2865,6 +2928,9 @@ func usedImportAliases(program *Program) map[string]bool { markType(param.Type) } markType(fn.ReturnType) + if fn.ExpressionBody != nil { + walkExpr(fn.ExpressionBody) + } for _, stmt := range fn.Body { walkStmt(stmt) } diff --git a/internal/lang/hir.go b/internal/lang/hir.go index 7d7044a..b52b8ea 100644 --- a/internal/lang/hir.go +++ b/internal/lang/hir.go @@ -109,9 +109,11 @@ type HIRMapping struct { func (HIRMapping) hirNode() {} type HIRFunction struct { - Symbol *Symbol - Decl *FunctionDecl - Scope *Scope + Symbol *Symbol + Decl *FunctionDecl + Scope *Scope + Expression *HIRExpr + Result Type } type HIRProgram struct { @@ -249,8 +251,19 @@ func (resolver *semanticResolver) resolve() error { _ = scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ}) } result, _ := resolver.program.ResolveTypeRefWithParams(decl.ReturnRef, decl.TypeParams) - resolver.resolveStmts(decl.Body, scope, nil, result) - resolver.program.HIR.Functions = append(resolver.program.HIR.Functions, &HIRFunction{Symbol: symbol, Decl: decl, Scope: scope}) + var expression *HIRExpr + if decl.ExpressionBody != nil { + expected := result + if decl.InferReturn { + expected = UnknownType{} + } + decl.ExpressionBody, result = resolver.resolveExpr(decl.ExpressionBody, scope, nil, expected) + expression = exprMeta(decl.ExpressionBody) + resolver.updateFunctionResult(symbol, result) + } else { + resolver.resolveStmts(decl.Body, scope, nil, result) + } + resolver.program.HIR.Functions = append(resolver.program.HIR.Functions, &HIRFunction{Symbol: symbol, Decl: decl, Scope: scope, Expression: expression, Result: result}) } for classIndex := range resolver.program.Syntax.Classes { decl := &resolver.program.Syntax.Classes[classIndex] @@ -266,13 +279,69 @@ func (resolver *semanticResolver) resolve() error { } params := append(append([]string{}, decl.TypeParams...), method.TypeParams...) result, _ := resolver.program.ResolveTypeRefWithParams(method.ReturnRef, params) - resolver.resolveStmts(method.Body, scope, class, result) - resolver.program.HIR.Methods = append(resolver.program.HIR.Methods, &HIRFunction{Symbol: class.Methods[method.Name], Decl: method, Scope: scope}) + var expression *HIRExpr + if method.ExpressionBody != nil { + expected := result + if method.InferReturn { + expected = UnknownType{} + } + method.ExpressionBody, result = resolver.resolveExpr(method.ExpressionBody, scope, class, expected) + expression = exprMeta(method.ExpressionBody) + resolver.updateFunctionResult(class.Methods[method.Name], result) + } else { + resolver.resolveStmts(method.Body, scope, class, result) + } + resolver.program.HIR.Methods = append(resolver.program.HIR.Methods, &HIRFunction{Symbol: class.Methods[method.Name], Decl: method, Scope: scope, Expression: expression, Result: result}) } } + resolver.inferExpressionReturns() return resolver.err } +func (resolver *semanticResolver) inferExpressionReturns() { + changed := true + for changed { + changed = false + for _, function := range append(append([]*HIRFunction{}, resolver.program.HIR.Functions...), resolver.program.HIR.Methods...) { + if function.Decl == nil || !function.Decl.InferReturn || function.Decl.ExpressionBody == nil { + continue + } + var class *ClassSymbol + if function.Symbol != nil { + for _, candidate := range resolver.program.ClassInfo { + if candidate.Method(function.Symbol.Name) == function.Symbol { + class = candidate + break + } + } + } + result := resolver.program.TypeOf(function.Decl.ExpressionBody, TypeEnvironment{Scope: function.Scope, Class: class}) + if isUnknownType(result) { + continue + } + current := function.Result + if isUnknownType(current) || !typeEqual(current, result) { + function.Result = result + if function.Expression != nil { + function.Expression.Type = result + } + resolver.updateFunctionResult(function.Symbol, result) + changed = true + } + } + } +} + +func (resolver *semanticResolver) updateFunctionResult(symbol *Symbol, result Type) { + if symbol == nil { + return + } + if function, ok := symbol.Type.(FunctionType); ok { + function.Result = result + symbol.Type = function + } +} + func (resolver *semanticResolver) resolveStmts(stmts []Stmt, scope *Scope, class *ClassSymbol, returnType Type) { for index, stmt := range stmts { switch value := stmt.(type) { @@ -768,7 +837,7 @@ var semanticBuiltins = map[string]bool{ "make": true, "new": true, "copy": true, "delete": true, "close": true, "panic": true, "recover": true, "string": true, "int": true, "float64": true, "bool": true, "sql": true, "set": true, "now": true, "Result": true, "ByteSlice": true, - "runBlocking": true, "coroutineScope": true, "launch": true, "async": true, + "runBlocking": true, "withContext": true, "coroutineScope": true, "launch": true, "async": true, "delay": true, "withTimeout": true, "isActive": true, "coroutineContext": true, "continue": true, "break": true, } diff --git a/internal/lang/metadata.go b/internal/lang/metadata.go index 5bb8cfc..1408f1e 100644 --- a/internal/lang/metadata.go +++ b/internal/lang/metadata.go @@ -65,7 +65,13 @@ func BuildPackageMetadata(program *Program, importPath string) (*PackageMetadata } for methodIndex := range class.Methods { method := &class.Methods[methodIndex] - item.Methods = append(item.Methods, metadataFunction(method, semantic.FunctionEffects[class.Name+"."+method.Name])) + result := Type(nil) + if symbol := semantic.ClassInfo[class.Name].Method(method.Name); symbol != nil { + if signature, ok := symbol.Type.(FunctionType); ok { + result = signature.Result + } + } + item.Methods = append(item.Methods, metadataFunction(method, semantic.FunctionEffects[class.Name+"."+method.Name], result)) } metadata.Classes = append(metadata.Classes, item) } @@ -78,13 +84,23 @@ func BuildPackageMetadata(program *Program, importPath string) (*PackageMetadata } for index := range program.Functions { function := &program.Functions[index] - metadata.Functions = append(metadata.Functions, metadataFunction(function, semantic.FunctionEffects[function.Name])) + result := Type(nil) + if symbol, ok := semantic.Global.Lookup(function.Name); ok { + if signature, ok := symbol.Type.(FunctionType); ok { + result = signature.Result + } + } + metadata.Functions = append(metadata.Functions, metadataFunction(function, semantic.FunctionEffects[function.Name], result)) } return metadata, nil } -func metadataFunction(function *FunctionDecl, effects Effect) FunctionMetadata { - item := FunctionMetadata{Name: function.Name, TypeParams: function.TypeParams, Result: function.ReturnType, Effects: effects} +func metadataFunction(function *FunctionDecl, effects Effect, result Type) FunctionMetadata { + resultName := function.ReturnType + if result != nil && !isUnknownType(result) { + resultName = result.String() + } + item := FunctionMetadata{Name: function.Name, TypeParams: function.TypeParams, Result: resultName, Effects: effects} for _, param := range function.Params { item.Params = append(item.Params, ParamMetadata{Name: param.Name, Type: param.Type}) } diff --git a/internal/lang/parser.go b/internal/lang/parser.go index bc7f9e0..904913f 100644 --- a/internal/lang/parser.go +++ b/internal/lang/parser.go @@ -471,18 +471,28 @@ func (p *parser) parseFunction() (FunctionDecl, error) { if err != nil { return FunctionDecl{}, err } + function := FunctionDecl{ + Name: signature.Name, + TypeParams: signature.TypeParams, + Params: signature.Params, + ReturnType: signature.ReturnType, + ReturnExplicit: signature.ReturnExplicit, + } + if p.match(tokenAssign) { + expression, err := p.parseExpr(0) + if err != nil { + return FunctionDecl{}, err + } + function.ExpressionBody = expression + function.InferReturn = !signature.ReturnExplicit + return function, nil + } body, err := p.parseBlock() if err != nil { return FunctionDecl{}, err } - - return FunctionDecl{ - Name: signature.Name, - TypeParams: signature.TypeParams, - Params: signature.Params, - ReturnType: signature.ReturnType, - Body: body, - }, nil + function.Body = body + return function, nil } func (p *parser) parseFunctionSignature() (FunctionSignature, error) { @@ -509,7 +519,9 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) { } returnType := "Unit" + returnExplicit := false if p.match(tokenColon) { + returnExplicit = true typ, err := p.parseTypeRef() if err != nil { return FunctionSignature{}, err @@ -518,10 +530,11 @@ func (p *parser) parseFunctionSignature() (FunctionSignature, error) { } return FunctionSignature{ - Name: name.lexeme, - TypeParams: typeParams, - Params: params, - ReturnType: returnType, + Name: name.lexeme, + TypeParams: typeParams, + Params: params, + ReturnType: returnType, + ReturnExplicit: returnExplicit, }, nil } diff --git a/internal/lang/semantics.go b/internal/lang/semantics.go index 6d764fc..37b9812 100644 --- a/internal/lang/semantics.go +++ b/internal/lang/semantics.go @@ -40,6 +40,9 @@ func (checker *mutabilityChecker) checkFunction(function *FunctionDecl, class *C typ, _ := checker.semantic.ResolveTypeRefWithParams(param.TypeRef, typeParams) _ = checker.scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ}) } + if function.ExpressionBody != nil { + return checker.checkExpr(function.ExpressionBody) + } return checker.checkStmts(function.Body) } diff --git a/internal/lang/sql.go b/internal/lang/sql.go index 924c9ea..449cd46 100644 --- a/internal/lang/sql.go +++ b/internal/lang/sql.go @@ -1221,12 +1221,18 @@ func stmtsMatch(stmts []Stmt, match func(Expr) bool) bool { func programExprMatches(program *Program, match func(Expr) bool) bool { for _, fn := range program.Functions { + if fn.ExpressionBody != nil && exprMatches(fn.ExpressionBody, match) { + return true + } if stmtsMatch(fn.Body, match) { return true } } for _, class := range program.Classes { for _, fn := range class.Methods { + if fn.ExpressionBody != nil && exprMatches(fn.ExpressionBody, match) { + return true + } if stmtsMatch(fn.Body, match) { return true } diff --git a/internal/lang/symbols.go b/internal/lang/symbols.go index a69a8a9..6a1e6d7 100644 --- a/internal/lang/symbols.go +++ b/internal/lang/symbols.go @@ -188,6 +188,11 @@ func analyzeProgram(program *Program, additional []*Program, metadata []*Package if err != nil { return nil, fmt.Errorf("function %s: %w", decl.Name, err) } + if decl.InferReturn { + function := typ.(FunctionType) + function.Result = UnknownType{} + typ = function + } if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: FunctionSymbolKind, Type: typ, Decl: decl}); err != nil { return nil, err } @@ -215,6 +220,11 @@ func analyzeProgram(program *Program, additional []*Program, metadata []*Package if err != nil { return nil, fmt.Errorf("method %s.%s: %w", decl.Name, method.Name, err) } + if method.InferReturn { + function := typ.(FunctionType) + function.Result = UnknownType{} + typ = function + } class.Methods[method.Name] = &Symbol{Name: method.Name, Kind: FunctionSymbolKind, Type: typ, Decl: method} } } diff --git a/internal/lang/type_checker.go b/internal/lang/type_checker.go index f25922e..df31d7b 100644 --- a/internal/lang/type_checker.go +++ b/internal/lang/type_checker.go @@ -134,6 +134,8 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) } case "coroutineContext": return NamedType{Name: "context.Context"} + case "withContext": + return NamedType{Name: "Unit"} } 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 { diff --git a/internal/lang/type_refs.go b/internal/lang/type_refs.go index 60a5faa..2f44a71 100644 --- a/internal/lang/type_refs.go +++ b/internal/lang/type_refs.go @@ -82,6 +82,11 @@ func hydrateFunctionRefs(function *FunctionDecl) error { return err } function.ReturnRef = ref + if function.ExpressionBody != nil { + if err := hydrateExprTypeRefs(function.ExpressionBody); err != nil { + return err + } + } return hydrateStmtTypeRefs(function.Body) } diff --git a/tools/vscode-gotlin/scripts/validate.js b/tools/vscode-gotlin/scripts/validate.js index 7fc1149..93c1c2b 100644 --- a/tools/vscode-gotlin/scripts/validate.js +++ b/tools/vscode-gotlin/scripts/validate.js @@ -82,7 +82,7 @@ for (const declaration of [ const prefixes = new Set(Object.values(snippets).map((snippet) => snippet.prefix)); for (const prefix of [ - "dataclass", "genericfun", "genericclass", "tablerow", "embed", "coroutinescope", "launch", "async", "enum", "match", "matchvalue", "mapto", "safe", "nonnull", "resultfun", "resultmatch", "defer", "foreach", "sqlfetch", + "dataclass", "exprfun", "withcontext", "genericfun", "genericclass", "tablerow", "embed", "coroutinescope", "launch", "async", "enum", "match", "matchvalue", "mapto", "safe", "nonnull", "resultfun", "resultmatch", "defer", "foreach", "sqlfetch", "sqlsingle", "sqliterator", "sqlinsertnothing", "sqlinsertupdate", "sqlupdatereturning" ]) { assert(prefixes.has(prefix), `snippets are missing prefix ${prefix}`); diff --git a/tools/vscode-gotlin/snippets/gotlin.code-snippets b/tools/vscode-gotlin/snippets/gotlin.code-snippets index 895d2cf..9272ee9 100644 --- a/tools/vscode-gotlin/snippets/gotlin.code-snippets +++ b/tools/vscode-gotlin/snippets/gotlin.code-snippets @@ -8,6 +8,16 @@ ], "description": "Gotlin function" }, + "Expression Function": { + "prefix": "exprfun", + "body": ["fun ${1:name}(${2}) = ${3:expression}"], + "description": "Gotlin expression-bodied function" + }, + "With Go Context": { + "prefix": "withcontext", + "body": ["withContext(${1:request}.context()) {", " $0", "}"], + "description": "Establish an ambient coroutine context from a Go context" + }, "Main Function": { "prefix": "main", "body": [