Introduce typed semantic analysis pipeline

This commit is contained in:
pavel 2026-08-27 19:28:24 +02:00
commit f4cd4f4458
30 changed files with 2079 additions and 2381 deletions

View file

@ -6,7 +6,6 @@ type Program struct {
Interfaces []InterfaceDecl
Enums []EnumDecl
Classes []ClassDecl
Workers []WorkerDecl
Functions []FunctionDecl
Embeds []EmbedDecl
}
@ -44,19 +43,6 @@ type ClassDecl struct {
Methods []FunctionDecl
}
type WorkerDecl struct {
Name string
Fields []WorkerFieldDecl
Methods []FunctionDecl
}
type WorkerFieldDecl struct {
Mutable bool
Name string
Type string
Value Expr
}
type FieldDecl struct {
Mutable bool
Private bool
@ -148,12 +134,6 @@ type ThrowStmt struct {
func (ThrowStmt) stmtNode() {}
type GoStmt struct {
Value Expr
}
func (GoStmt) stmtNode() {}
type DeferStmt struct {
Value Expr
}
@ -198,12 +178,6 @@ type TryCatchStmt struct {
func (TryCatchStmt) stmtNode() {}
type SelectStmt struct {
Cases []SelectCase
}
func (SelectStmt) stmtNode() {}
type MatchStmt struct {
Value Expr
Cases []MatchCase
@ -218,6 +192,7 @@ type MatchCase struct {
}
type MatchExpr struct {
Meta ExprMeta
Value Expr
Cases []MatchExprCase
}
@ -230,46 +205,47 @@ type MatchExprCase struct {
Value Expr
}
type SelectCase struct {
Source Expr
Body []Stmt
}
type IdentExpr struct {
Meta ExprMeta
Name string
}
func (IdentExpr) exprNode() {}
type IntExpr struct {
Meta ExprMeta
Value string
}
func (IntExpr) exprNode() {}
type FloatExpr struct {
Meta ExprMeta
Value string
}
func (FloatExpr) exprNode() {}
type StringExpr struct {
Meta ExprMeta
Value string
}
func (StringExpr) exprNode() {}
type BoolExpr struct {
Meta ExprMeta
Value bool
}
func (BoolExpr) exprNode() {}
type NullExpr struct{}
type NullExpr struct{ Meta ExprMeta }
func (NullExpr) exprNode() {}
type UnaryExpr struct {
Meta ExprMeta
Op string
Value Expr
}
@ -277,6 +253,7 @@ type UnaryExpr struct {
func (UnaryExpr) exprNode() {}
type BinaryExpr struct {
Meta ExprMeta
Left Expr
Op string
Right Expr
@ -285,6 +262,7 @@ type BinaryExpr struct {
func (BinaryExpr) exprNode() {}
type CallExpr struct {
Meta ExprMeta
Callee Expr
Args []Expr
TypeArgs []string
@ -299,6 +277,7 @@ type NamedArg struct {
func (CallExpr) exprNode() {}
type SelectorExpr struct {
Meta ExprMeta
Receiver Expr
Name string
}
@ -306,21 +285,29 @@ type SelectorExpr struct {
func (SelectorExpr) exprNode() {}
type SafeSelectorExpr struct {
Meta ExprMeta
Receiver Expr
Name string
}
func (SafeSelectorExpr) exprNode() {}
type NonNullExpr struct{ Value Expr }
type NonNullExpr struct {
Meta ExprMeta
Value Expr
}
func (NonNullExpr) exprNode() {}
type TryExpr struct{ Value Expr }
type TryExpr struct {
Meta ExprMeta
Value Expr
}
func (TryExpr) exprNode() {}
type IndexExpr struct {
Meta ExprMeta
Receiver Expr
Index Expr
}
@ -328,6 +315,7 @@ type IndexExpr struct {
func (IndexExpr) exprNode() {}
type EnumVariantExpr struct {
Meta ExprMeta
EnumName, VariantName string
Values []Expr
}
@ -335,6 +323,7 @@ type EnumVariantExpr struct {
func (EnumVariantExpr) exprNode() {}
type LambdaExpr struct {
Meta ExprMeta
Params []Param
ImplicitIt bool
Body []Stmt

View file

@ -1096,7 +1096,7 @@ fun main() {
}
}
func TestGenerateGoChannelsAndSelect(t *testing.T) {
func TestGenerateGoChannelSendAndRead(t *testing.T) {
src := `
package demo
@ -1105,13 +1105,8 @@ fun writer(ch: Channel<Int>) {
}
fun main() {
val ch = Channel<Int>()
runBlocking {
launch { writer(ch) }
select {
ch -> println(it)
}
}
val ch = Channel<Int>(1)
writer(ch)
val v = ch.read()
println(v)
}
@ -1131,12 +1126,8 @@ fun main() {
for _, want := range []string{
`func writer(ch chan int)`,
`ch <- 7`,
`ch := make(chan int)`,
`gotlinScope.Launch`,
`ch := make(chan int, 1)`,
`writer(ch)`,
`select {`,
`case it := <-ch:`,
`fmt.Println(it)`,
`v := <-ch`,
`fmt.Println(v)`,
} {
@ -1146,138 +1137,6 @@ fun main() {
}
}
func TestGenerateGoSelectAfterAndEvery(t *testing.T) {
src := `
package demo
fun main() {
select {
after(1000) -> println(it)
every(250) -> println("tick")
}
}
`
prog, err := Parse(src)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
out, err := GenerateGo(prog)
if err != nil {
t.Fatalf("go generation failed: %v", err)
}
code := string(out)
for _, want := range []string{
`"time"`,
`func gotlinEveryMs(ms int) <-chan time.Time {`,
`case it := <-time.After(time.Duration(1000) * time.Millisecond):`,
`fmt.Println(it)`,
`case <-gotlinEveryMs(250):`,
`fmt.Println("tick")`,
} {
if !strings.Contains(code, want) {
t.Fatalf("generated Go missing %q:\n%s", want, code)
}
}
}
func TestGenerateGoEveryRejectsNonPositiveMilliseconds(t *testing.T) {
cases := []string{
`every(0)`,
`every(-1)`,
}
for _, timerCall := range cases {
src := `
package demo
fun main() {
select {
` + timerCall + ` -> println("x")
}
}
`
prog, err := Parse(src)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
_, err = GenerateGo(prog)
if err == nil {
t.Fatalf("expected go generation error for %s", timerCall)
}
if !strings.Contains(err.Error(), "every(ms) requires ms > 0") {
t.Fatalf("unexpected error for %s: %v", timerCall, err)
}
}
}
func TestGenerateGoAfterEveryArityValidation(t *testing.T) {
cases := []struct {
call string
want string
}{
{`after()`, "after(ms) expects exactly one Int argument"},
{`after(1, 2)`, "after(ms) expects exactly one Int argument"},
{`every()`, "every(ms) expects exactly one Int argument"},
{`every(1, 2)`, "every(ms) expects exactly one Int argument"},
}
for _, tc := range cases {
src := `
package demo
fun main() {
select {
` + tc.call + ` -> println("x")
}
}
`
prog, err := Parse(src)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
_, err = GenerateGo(prog)
if err == nil {
t.Fatalf("expected go generation error for %s", tc.call)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("unexpected error for %s: %v", tc.call, err)
}
}
}
func TestGenerateGoAfterEveryIntValidation(t *testing.T) {
cases := []struct {
call string
want string
}{
{`after("1s")`, "after(ms) expects an Int argument"},
{`every(true)`, "every(ms) expects an Int argument"},
}
for _, tc := range cases {
src := `
package demo
fun main() {
select {
` + tc.call + ` -> println("x")
}
}
`
prog, err := Parse(src)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
_, err = GenerateGo(prog)
if err == nil {
t.Fatalf("expected go generation error for %s", tc.call)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("unexpected error for %s: %v", tc.call, err)
}
}
}
func TestGenerateGoNoTimeImportWhenUnused(t *testing.T) {
src := `
package demo
@ -1300,36 +1159,6 @@ fun main() {
}
}
func TestGenerateGoNoTickerShorthandRewrite(t *testing.T) {
src := `
package demo
import time
fun main() {
val tick = time.NewTicker(time.Second)
select {
tick -> println("x")
}
}
`
prog, err := Parse(src)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
out, err := GenerateGo(prog)
if err != nil {
t.Fatalf("go generation failed: %v", err)
}
code := string(out)
if strings.Contains(code, "<-tick.C") {
t.Fatalf("ticker shorthand rewrite should be removed:\n%s", code)
}
if !strings.Contains(code, "case <-tick:") {
t.Fatalf("expected raw source receive in generated code:\n%s", code)
}
}
func TestGenerateGoExplicitErrorsForBunStyleCalls(t *testing.T) {
src := `
package demo

View file

@ -104,14 +104,14 @@ func (g *goGenerator) emitCoroutineSupport() {
func (g *goGenerator) coroutineLambda(lambda LambdaExpr, returnType string) (string, error) {
var b strings.Builder
b.WriteString("func(gotlinScope *GotlinCoroutineScope)")
if mapped := mapGoType(returnType); mapped != "" {
if mapped := g.goType(returnType); mapped != "" {
b.WriteString(" ")
b.WriteString(mapped)
}
b.WriteString(" {\n")
sub := goGenerator{indentLevel: 1, needsFmt: g.needsFmt, needsTime: g.needsTime, needsCoroutines: true, functions: g.functions, classes: g.classes, workers: g.workers, enums: g.enums, imports: g.imports, currentFunc: FunctionDecl{ReturnType: returnType}, currentClass: g.currentClass, currentWorker: g.currentWorker, currentCoroutineScope: "gotlinScope", mappings: g.mappings}
sub := goGenerator{semantic: g.semantic, indentLevel: 1, needsFmt: g.needsFmt, needsTime: g.needsTime, needsCoroutines: true, currentFunc: FunctionDecl{ReturnType: returnType}, currentClass: g.currentClass, currentCoroutineScope: "gotlinScope"}
sub.scopes = g.cloneScopes()
sub.typeScopes = g.cloneTypeScopes()
sub.semanticScope = g.semanticScope
sub.pushScope()
if err := sub.block(lambda.Body); err != nil {
return "", err

View file

@ -44,8 +44,8 @@ fun main() { println(load()) }`)
}
}
func TestWorkerAndBareGoAreRemoved(t *testing.T) {
for _, source := range []string{`package demo worker Counter { var count = 0 }`, `package demo fun main() { go println("x") }`} {
func TestLegacyConcurrencySyntaxIsRemoved(t *testing.T) {
for _, source := range []string{`package demo worker Counter { var count = 0 }`, `package demo fun main() { go println("x") }`, `package demo fun main() { select { channel -> println(it) } }`} {
if _, err := Parse(source); err == nil {
t.Fatalf("deprecated concurrency syntax parsed: %s", source)
}

File diff suppressed because it is too large Load diff

170
internal/lang/go_types.go Normal file
View file

@ -0,0 +1,170 @@
package lang
import (
"go/importer"
gotypes "go/types"
"strings"
)
func importGoPackage(decl ImportDecl) (*gotypes.Package, error) {
path := strings.Trim(decl.Path, `"`)
if !strings.Contains(path, "/") {
path = importPathToGoPath(path)
}
return importer.Default().Import(path)
}
func (semantic *SemanticProgram) goSelectorType(alias, name string) Type {
pkg := semantic.GoPackages[alias]
if pkg == nil {
return UnknownType{}
}
object := pkg.Scope().Lookup(exportedGoName(name))
if object == nil {
return UnknownType{}
}
if function, ok := object.(*gotypes.Func); ok {
if signature, ok := function.Type().(*gotypes.Signature); ok {
return semanticTypeFromGoSignature(signature)
}
}
return semanticTypeFromGo(object.Type())
}
func (semantic *SemanticProgram) goMethodType(receiver Type, name string) Type {
text := receiver.String()
pointer := strings.HasPrefix(text, "*")
text = strings.TrimPrefix(text, "*")
parts := strings.Split(text, ".")
if len(parts) != 2 {
return UnknownType{}
}
for _, pkg := range semantic.GoPackages {
if pkg.Name() != parts[0] {
continue
}
object, ok := pkg.Scope().Lookup(parts[1]).(*gotypes.TypeName)
if !ok {
return UnknownType{}
}
var typ gotypes.Type = object.Type()
if pointer {
typ = gotypes.NewPointer(typ)
}
selection := gotypes.NewMethodSet(typ).Lookup(pkg, exportedGoName(name))
if selection == nil {
return UnknownType{}
}
if signature, ok := selection.Obj().Type().(*gotypes.Signature); ok {
return semanticTypeFromGoSignature(signature)
}
}
return UnknownType{}
}
func (semantic *SemanticProgram) goFieldType(receiver Type, name string) Type {
text := strings.TrimPrefix(receiver.String(), "*")
parts := strings.Split(text, ".")
if len(parts) != 2 {
return UnknownType{}
}
for _, pkg := range semantic.GoPackages {
if pkg.Name() != parts[0] {
continue
}
object, ok := pkg.Scope().Lookup(parts[1]).(*gotypes.TypeName)
if !ok {
return UnknownType{}
}
underlying, ok := object.Type().Underlying().(*gotypes.Struct)
if !ok {
return UnknownType{}
}
for index := 0; index < underlying.NumFields(); index++ {
field := underlying.Field(index)
if field.Name() == exportedGoName(name) {
return semanticTypeFromGo(field.Type())
}
}
}
return UnknownType{}
}
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}
}
func semanticTypeFromGoResults(results *gotypes.Tuple) Type {
if results == nil || results.Len() == 0 {
return NamedType{Name: "Unit"}
}
last := semanticTypeFromGo(results.At(results.Len() - 1).Type())
if last.String() == "Error" {
if results.Len() == 1 {
return NullableType{Element: NamedType{Name: "Error"}}
}
value := Type(NamedType{Name: "Unit"})
if results.Len() == 2 {
value = semanticTypeFromGo(results.At(0).Type())
} else if results.Len() > 2 {
return UnknownType{}
}
return GenericType{Base: NamedType{Name: "Result"}, Args: []Type{value, NamedType{Name: "Error"}}}
}
if results.Len() == 1 {
return last
}
return UnknownType{}
}
func semanticTypeFromGo(typ gotypes.Type) Type {
switch value := typ.(type) {
case *gotypes.Basic:
switch value.Kind() {
case gotypes.Bool:
return NamedType{Name: "Boolean"}
case gotypes.String:
return NamedType{Name: "String"}
case gotypes.Int64, gotypes.Uint64:
return NamedType{Name: "Long"}
case gotypes.Float32, gotypes.Float64:
return NamedType{Name: "Double"}
case gotypes.Int, gotypes.Int8, gotypes.Int16, gotypes.Int32, gotypes.Uint, gotypes.Uint8, gotypes.Uint16, gotypes.Uint32:
return NamedType{Name: "Int"}
case gotypes.UnsafePointer:
return NamedType{Name: "Any"}
}
case *gotypes.Pointer:
return GoPointerType{Element: semanticTypeFromGo(value.Elem())}
case *gotypes.Slice:
if basic, ok := value.Elem().(*gotypes.Basic); ok && basic.Kind() == gotypes.Byte {
return NamedType{Name: "ByteSlice"}
}
return GenericType{Base: NamedType{Name: "List"}, Args: []Type{semanticTypeFromGo(value.Elem())}}
case *gotypes.Map:
return GenericType{Base: NamedType{Name: "Map"}, Args: []Type{semanticTypeFromGo(value.Key()), semanticTypeFromGo(value.Elem())}}
case *gotypes.Chan:
return GenericType{Base: NamedType{Name: "Channel"}, Args: []Type{semanticTypeFromGo(value.Elem())}}
case *gotypes.Signature:
return semanticTypeFromGoSignature(value)
case *gotypes.Named:
if value.Obj().Pkg() == nil && value.Obj().Name() == "error" {
return NamedType{Name: "Error"}
}
if value.Obj().Pkg() != nil {
return NamedType{Name: value.Obj().Pkg().Name() + "." + value.Obj().Name()}
}
return NamedType{Name: value.Obj().Name()}
case *gotypes.Interface:
if value.String() == "error" {
return NamedType{Name: "Error"}
}
return NamedType{Name: "Any"}
}
return UnknownType{}
}

765
internal/lang/hir.go Normal file
View file

@ -0,0 +1,765 @@
package lang
import "fmt"
type ExprMeaning int
const (
UnresolvedExpr ExprMeaning = iota
LiteralExpr
LocalReferenceExpr
FunctionReferenceExpr
ClassReferenceExpr
EnumReferenceExpr
ImportReferenceExpr
GotlinCallExpr
GoCallExpr
ClassConstructionExpr
EnumConstructionExpr
FieldAccessExpr
MethodCallExpr
PropagateResultExpr
MatchValueExpr
SQLExpression
MappingExpression
CoroutineExpression
)
type ExprMeta struct{ Semantic *HIRExpr }
type HIRExpr struct {
Type Type
Meaning ExprMeaning
Symbol *Symbol
Node HIRNode
}
type HIRNode interface{ hirNode() }
type HIRLiteral struct{}
func (HIRLiteral) hirNode() {}
type HIRReference struct{ Target *Symbol }
func (HIRReference) hirNode() {}
type HIRGoCall struct {
Callee *HIRExpr
Result Type
}
func (HIRGoCall) hirNode() {}
type HIRGotlinCall struct {
Target *Symbol
Result Type
}
func (HIRGotlinCall) hirNode() {}
type HIRClassConstruction struct{ Class *ClassSymbol }
func (HIRClassConstruction) hirNode() {}
type HIREnumConstruction struct {
EnumName, VariantName string
}
func (HIREnumConstruction) hirNode() {}
type HIRPropagateResult struct {
Value *HIRExpr
Type Type
}
func (HIRPropagateResult) hirNode() {}
type HIRMatch struct {
Value *HIRExpr
Cases []HIRMatchCase
Type Type
}
func (HIRMatch) hirNode() {}
type HIRMatchCase struct {
EnumName, VariantName string
Bindings []string
Value *HIRExpr
}
type HIRCoroutine struct{ Operation string }
func (HIRCoroutine) hirNode() {}
type HIRSQL struct{ Type Type }
func (HIRSQL) hirNode() {}
type HIRMapping struct {
Source Type
Target Type
}
func (HIRMapping) hirNode() {}
type HIRFunction struct {
Symbol *Symbol
Decl *FunctionDecl
Scope *Scope
}
type HIRProgram struct {
Functions []*HIRFunction
Methods []*HIRFunction
}
func exprMeta(expr Expr) *HIRExpr {
switch value := expr.(type) {
case IdentExpr:
return value.Meta.Semantic
case IntExpr:
return value.Meta.Semantic
case FloatExpr:
return value.Meta.Semantic
case StringExpr:
return value.Meta.Semantic
case BoolExpr:
return value.Meta.Semantic
case NullExpr:
return value.Meta.Semantic
case UnaryExpr:
return value.Meta.Semantic
case BinaryExpr:
return value.Meta.Semantic
case CallExpr:
return value.Meta.Semantic
case SelectorExpr:
return value.Meta.Semantic
case SafeSelectorExpr:
return value.Meta.Semantic
case NonNullExpr:
return value.Meta.Semantic
case TryExpr:
return value.Meta.Semantic
case IndexExpr:
return value.Meta.Semantic
case EnumVariantExpr:
return value.Meta.Semantic
case MatchExpr:
return value.Meta.Semantic
case LambdaExpr:
return value.Meta.Semantic
default:
return nil
}
}
func ResolvedType(expr Expr) Type {
if semantic := exprMeta(expr); semantic != nil {
return semantic.Type
}
return UnknownType{}
}
func ResolvedMeaning(expr Expr) ExprMeaning {
if semantic := exprMeta(expr); semantic != nil {
return semantic.Meaning
}
return UnresolvedExpr
}
func withExprMeta(expr Expr, semantic *HIRExpr) Expr {
switch value := expr.(type) {
case IdentExpr:
value.Meta.Semantic = semantic
return value
case IntExpr:
value.Meta.Semantic = semantic
return value
case FloatExpr:
value.Meta.Semantic = semantic
return value
case StringExpr:
value.Meta.Semantic = semantic
return value
case BoolExpr:
value.Meta.Semantic = semantic
return value
case NullExpr:
value.Meta.Semantic = semantic
return value
case UnaryExpr:
value.Meta.Semantic = semantic
return value
case BinaryExpr:
value.Meta.Semantic = semantic
return value
case CallExpr:
value.Meta.Semantic = semantic
return value
case SelectorExpr:
value.Meta.Semantic = semantic
return value
case SafeSelectorExpr:
value.Meta.Semantic = semantic
return value
case NonNullExpr:
value.Meta.Semantic = semantic
return value
case TryExpr:
value.Meta.Semantic = semantic
return value
case IndexExpr:
value.Meta.Semantic = semantic
return value
case EnumVariantExpr:
value.Meta.Semantic = semantic
return value
case MatchExpr:
value.Meta.Semantic = semantic
return value
case LambdaExpr:
value.Meta.Semantic = semantic
return value
default:
return expr
}
}
type semanticResolver struct {
program *SemanticProgram
err error
}
func (resolver *semanticResolver) resolve() error {
resolver.program.HIR = &HIRProgram{}
for index := range resolver.program.Syntax.Functions {
decl := &resolver.program.Syntax.Functions[index]
symbol, _ := resolver.program.Global.Lookup(decl.Name)
scope := NewScope(resolver.program.Global)
for _, param := range decl.Params {
typ, _ := resolver.program.ResolveType(param.Type)
_ = scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
}
result, _ := resolver.program.ResolveType(decl.ReturnType)
resolver.resolveStmts(decl.Body, scope, nil, result)
resolver.program.HIR.Functions = append(resolver.program.HIR.Functions, &HIRFunction{Symbol: symbol, Decl: decl, Scope: scope})
}
for classIndex := range resolver.program.Syntax.Classes {
decl := &resolver.program.Syntax.Classes[classIndex]
class := resolver.program.ClassInfo[decl.Name]
for methodIndex := range decl.Methods {
method := &decl.Methods[methodIndex]
scope := NewScope(resolver.program.Global)
_ = scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}})
for _, param := range method.Params {
typ, _ := resolver.program.ResolveType(param.Type)
_ = scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
}
result, _ := resolver.program.ResolveType(method.ReturnType)
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})
}
}
return resolver.err
}
func (resolver *semanticResolver) resolveStmts(stmts []Stmt, scope *Scope, class *ClassSymbol, returnType Type) {
for index, stmt := range stmts {
switch value := stmt.(type) {
case VarDecl:
expected := Type(UnknownType{})
if value.Type != "" {
expected, _ = resolver.program.ResolveType(value.Type)
}
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, expected)
if isUnknownType(expected) {
expected = exprMeta(value.Value).Type
}
_ = scope.Define(&Symbol{Name: value.Name, Kind: VariableSymbol, Type: expected, Mutable: value.Mutable, Decl: &value})
stmts[index] = value
case MultiVarDecl:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
valueTypes := make([]Type, len(value.Names))
for index := range valueTypes {
valueTypes[index] = UnknownType{}
}
if result, ok := ResolvedType(value.Value).(GenericType); ok && result.Base.String() == "Result" && len(result.Args) == 2 && len(valueTypes) == 2 {
valueTypes[0] = result.Args[0]
valueTypes[1] = NullableType{Element: result.Args[1]}
}
for index, name := range value.Names {
_ = scope.Define(&Symbol{Name: name, Kind: VariableSymbol, Type: valueTypes[index], Mutable: value.Mutable})
}
stmts[index] = value
case AssignStmt:
expected := Type(UnknownType{})
if symbol, ok := scope.Lookup(value.Name); ok {
expected = symbol.Type
}
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, expected)
stmts[index] = value
case AddAssignStmt:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
stmts[index] = value
case MultiAssignStmt:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
stmts[index] = value
case ReturnStmt:
if value.Value != nil {
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, returnType)
stmts[index] = value
}
case ThrowStmt:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, NamedType{Name: "Error"})
stmts[index] = value
case DeferStmt:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, NamedType{Name: "Unit"})
stmts[index] = value
case ExprStmt:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
stmts[index] = value
case IfStmt:
value.Cond, _ = resolver.resolveExpr(value.Cond, scope, class, NamedType{Name: "Boolean"})
resolver.resolveStmts(value.Then, NewScope(scope), class, returnType)
resolver.resolveStmts(value.Else, NewScope(scope), class, returnType)
stmts[index] = value
case WhileStmt:
value.Cond, _ = resolver.resolveExpr(value.Cond, scope, class, NamedType{Name: "Boolean"})
resolver.resolveStmts(value.Body, NewScope(scope), class, returnType)
stmts[index] = value
case ForEachStmt:
value.Source, _ = resolver.resolveExpr(value.Source, scope, class, UnknownType{})
bodyScope := NewScope(scope)
element := collectionElement(exprMeta(value.Source).Type)
_ = bodyScope.Define(&Symbol{Name: value.Name, Kind: VariableSymbol, Type: element})
resolver.resolveStmts(value.Body, bodyScope, class, returnType)
stmts[index] = value
case MatchStmt:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
for caseIndex := range value.Cases {
matchCase := &value.Cases[caseIndex]
caseScope := NewScope(scope)
resolver.defineMatchBindings(caseScope, matchCase.EnumName, matchCase.VariantName, matchCase.Bindings)
resolver.resolveStmts(matchCase.Body, caseScope, class, returnType)
}
resolver.validateStatementMatch(value)
stmts[index] = value
case TryCatchStmt:
resolver.resolveStmts(value.TryBody, NewScope(scope), class, returnType)
catchScope := NewScope(scope)
catchType, _ := resolver.program.ResolveType(value.CatchType)
_ = catchScope.Define(&Symbol{Name: value.CatchName, Kind: VariableSymbol, Type: catchType})
resolver.resolveStmts(value.CatchBody, catchScope, class, returnType)
}
}
}
func (resolver *semanticResolver) resolveExpr(expr Expr, scope *Scope, class *ClassSymbol, expected Type) (Expr, Type) {
environment := TypeEnvironment{Scope: scope, Class: class}
switch value := expr.(type) {
case UnaryExpr:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
expr = value
case BinaryExpr:
value.Left, _ = resolver.resolveExpr(value.Left, scope, class, UnknownType{})
value.Right, _ = resolver.resolveExpr(value.Right, scope, class, UnknownType{})
expr = value
case CallExpr:
value.Callee, _ = resolver.resolveExpr(value.Callee, scope, class, UnknownType{})
for index := range value.Args {
argumentExpected := Type(UnknownType{})
if selector, ok := value.Callee.(SelectorExpr); ok {
if receiver, ok := selector.Receiver.(IdentExpr); ok && receiver.Name == "Result" {
if selector.Name == "Err" {
argumentExpected = NamedType{Name: "Error"}
} else if selector.Name == "Ok" {
if result, ok := expected.(GenericType); ok && result.Base.String() == "Result" && len(result.Args) == 2 {
argumentExpected = result.Args[0]
}
}
}
}
value.Args[index], _ = resolver.resolveExpr(value.Args[index], scope, class, argumentExpected)
}
for index := range value.NamedArgs {
value.NamedArgs[index].Value, _ = resolver.resolveExpr(value.NamedArgs[index].Value, scope, class, UnknownType{})
}
expr = value
case SelectorExpr:
value.Receiver, _ = resolver.resolveExpr(value.Receiver, scope, class, UnknownType{})
expr = value
case SafeSelectorExpr:
value.Receiver, _ = resolver.resolveExpr(value.Receiver, scope, class, UnknownType{})
expr = value
case NonNullExpr:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
expr = value
case TryExpr:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
expr = value
case IndexExpr:
value.Receiver, _ = resolver.resolveExpr(value.Receiver, scope, class, UnknownType{})
value.Index, _ = resolver.resolveExpr(value.Index, scope, class, NamedType{Name: "Int"})
expr = value
case EnumVariantExpr:
for index := range value.Values {
value.Values[index], _ = resolver.resolveExpr(value.Values[index], scope, class, UnknownType{})
}
expr = value
case MatchExpr:
value.Value, _ = resolver.resolveExpr(value.Value, scope, class, UnknownType{})
for index := range value.Cases {
matchCase := &value.Cases[index]
caseScope := NewScope(scope)
resolver.defineMatchBindings(caseScope, matchCase.EnumName, matchCase.VariantName, matchCase.Bindings)
matchCase.Value, _ = resolver.resolveExpr(matchCase.Value, caseScope, class, expected)
}
resolver.validateValueMatch(value)
expr = value
case LambdaExpr:
lambdaScope := NewScope(scope)
for _, param := range value.Params {
typ, _ := resolver.program.ResolveType(param.Type)
_ = lambdaScope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
}
resolver.resolveStmts(value.Body, lambdaScope, class, functionResult(expected))
expr = value
}
typ := resolver.program.TypeOf(expr, environment)
if isUnknownType(typ) && !isUnknownType(expected) {
typ = expected
}
if binary, ok := expr.(BinaryExpr); ok {
switch binary.Op {
case "==", "!=", "<", "<=", ">", ">=", "&&", "||":
typ = NamedType{Name: "Boolean"}
default:
if meta := exprMeta(binary.Left); meta != nil {
typ = meta.Type
}
}
}
meaning, symbol := resolver.meaning(expr, scope)
resolver.validateEnumExpression(expr, meaning)
if meaning == GoCallExpr && isResultType(expected) {
typ = expected
}
if nullable, ok := typ.(NullableType); ok && typeEqual(nullable.Element, expected) {
typ = expected
}
semantic := &HIRExpr{Type: typ, Meaning: meaning, Symbol: symbol}
semantic.Node = resolver.hirNode(expr, semantic)
return withExprMeta(expr, semantic), typ
}
func (resolver *semanticResolver) hirNode(expr Expr, semantic *HIRExpr) HIRNode {
switch semantic.Meaning {
case LiteralExpr:
return HIRLiteral{}
case LocalReferenceExpr, FunctionReferenceExpr, ClassReferenceExpr, EnumReferenceExpr, ImportReferenceExpr, FieldAccessExpr:
return HIRReference{Target: semantic.Symbol}
case GoCallExpr:
if call, ok := expr.(CallExpr); ok {
return HIRGoCall{Callee: exprMeta(call.Callee), Result: semantic.Type}
}
case GotlinCallExpr, MethodCallExpr:
return HIRGotlinCall{Target: semantic.Symbol, Result: semantic.Type}
case ClassConstructionExpr:
if class := classTypeOf(semantic.Type); class != nil {
return HIRClassConstruction{Class: class}
}
case EnumConstructionExpr:
enumName, variantName := enumExpressionName(expr)
return HIREnumConstruction{EnumName: enumName, VariantName: variantName}
case PropagateResultExpr:
if attempt, ok := expr.(TryExpr); ok {
return HIRPropagateResult{Value: exprMeta(attempt.Value), Type: semantic.Type}
}
case MatchValueExpr:
if match, ok := expr.(MatchExpr); ok {
cases := make([]HIRMatchCase, len(match.Cases))
for index, matchCase := range match.Cases {
cases[index] = HIRMatchCase{EnumName: matchCase.EnumName, VariantName: matchCase.VariantName, Bindings: matchCase.Bindings, Value: exprMeta(matchCase.Value)}
}
return HIRMatch{Value: exprMeta(match.Value), Cases: cases, Type: semantic.Type}
}
case CoroutineExpression:
if call, ok := expr.(CallExpr); ok {
if ident, ok := call.Callee.(IdentExpr); ok {
return HIRCoroutine{Operation: ident.Name}
}
}
case SQLExpression:
return HIRSQL{Type: semantic.Type}
case MappingExpression:
if call, ok := expr.(CallExpr); ok {
if selector, ok := call.Callee.(SelectorExpr); ok {
target := semantic.Type
return HIRMapping{Source: ResolvedType(selector.Receiver), Target: target}
}
}
}
return nil
}
func enumExpressionName(expr Expr) (string, string) {
switch value := expr.(type) {
case CallExpr:
if selector, ok := value.Callee.(SelectorExpr); ok {
if receiver, ok := selector.Receiver.(IdentExpr); ok {
return receiver.Name, selector.Name
}
}
case SelectorExpr:
if receiver, ok := value.Receiver.(IdentExpr); ok {
return receiver.Name, value.Name
}
case EnumVariantExpr:
return value.EnumName, value.VariantName
}
return "", ""
}
func (resolver *semanticResolver) meaning(expr Expr, scope *Scope) (ExprMeaning, *Symbol) {
switch value := expr.(type) {
case IntExpr, FloatExpr, StringExpr, BoolExpr, NullExpr:
return LiteralExpr, nil
case IdentExpr:
if symbol, ok := scope.Lookup(value.Name); ok {
switch symbol.Kind {
case FunctionSymbolKind:
return FunctionReferenceExpr, symbol
case ClassSymbolKind:
return ClassReferenceExpr, symbol
case EnumSymbolKind:
return EnumReferenceExpr, symbol
case ImportSymbolKind:
return ImportReferenceExpr, symbol
default:
return LocalReferenceExpr, symbol
}
}
case CallExpr:
if _, _, _, ok := splitSQLChain(value); ok {
return SQLExpression, nil
}
if _, ok := value.Callee.(SelectorExpr); ok {
selector := value.Callee.(SelectorExpr)
if receiver, ok := selector.Receiver.(IdentExpr); ok && receiver.Name == "json" && selector.Name == "decode" {
return GotlinCallExpr, nil
}
if selector.Name == "mapTo" {
return MappingExpression, nil
}
if receiver, ok := selector.Receiver.(IdentExpr); ok {
if receiver.Name == "Result" {
return EnumConstructionExpr, nil
}
if _, ok := resolver.program.Enums[receiver.Name]; ok {
return EnumConstructionExpr, nil
}
}
if root, ok := selectorRootAlias(selector); ok && resolver.program.Imports[root] {
return GoCallExpr, nil
}
receiverType := ResolvedType(selector.Receiver)
if !isUnknownType(resolver.program.goMethodType(receiverType, selector.Name)) {
return GoCallExpr, nil
}
return MethodCallExpr, nil
}
if ident, ok := value.Callee.(IdentExpr); ok {
if coroutineBuiltins[ident.Name] {
return CoroutineExpression, nil
}
if symbol, ok := resolver.program.Global.Lookup(ident.Name); ok {
if symbol.Kind == ClassSymbolKind {
return ClassConstructionExpr, symbol
}
if symbol.Kind == FunctionSymbolKind {
return GotlinCallExpr, symbol
}
}
}
return GoCallExpr, nil
case SelectorExpr:
if receiver, ok := value.Receiver.(IdentExpr); ok {
if _, ok := resolver.program.Enums[receiver.Name]; ok {
return EnumReferenceExpr, nil
}
}
return FieldAccessExpr, nil
case SafeSelectorExpr:
return FieldAccessExpr, nil
case EnumVariantExpr:
return EnumConstructionExpr, nil
case TryExpr:
return PropagateResultExpr, nil
case MatchExpr:
return MatchValueExpr, nil
}
return UnresolvedExpr, nil
}
func (resolver *semanticResolver) defineMatchBindings(scope *Scope, enumName, variantName string, bindings []string) {
decl, ok := resolver.program.Enums[enumName]
if !ok {
return
}
variant := enumVariant(decl, variantName)
if variant == nil {
return
}
for index, binding := range bindings {
if index >= len(variant.PayloadTypes) {
break
}
typ, _ := resolver.program.ResolveType(variant.PayloadTypes[index])
_ = scope.Define(&Symbol{Name: binding, Kind: VariableSymbol, Type: typ})
}
}
func (resolver *semanticResolver) validateStatementMatch(match MatchStmt) {
patterns := make([]matchPattern, len(match.Cases))
for index, matchCase := range match.Cases {
patterns[index] = matchPattern{enumName: matchCase.EnumName, variantName: matchCase.VariantName, bindings: matchCase.Bindings}
}
resolver.validateMatchPatterns(patterns)
}
func (resolver *semanticResolver) validateValueMatch(match MatchExpr) {
patterns := make([]matchPattern, len(match.Cases))
var result Type = UnknownType{}
for index, matchCase := range match.Cases {
patterns[index] = matchPattern{enumName: matchCase.EnumName, variantName: matchCase.VariantName, bindings: matchCase.Bindings}
armType := ResolvedType(matchCase.Value)
if isUnknownType(result) {
result = armType
} else if !isUnknownType(armType) && !typeEqual(result, armType) {
resolver.fail(fmt.Errorf("match expression arm %s.%s has type %s, expected %s", matchCase.EnumName, matchCase.VariantName, armType.String(), result.String()))
}
}
if isUnknownType(result) {
resolver.fail(fmt.Errorf("match expression result type cannot be inferred"))
}
resolver.validateMatchPatterns(patterns)
}
type matchPattern struct {
enumName, variantName string
bindings []string
}
func (resolver *semanticResolver) validateMatchPatterns(patterns []matchPattern) {
if len(patterns) == 0 {
resolver.fail(fmt.Errorf("match requires at least one case"))
return
}
enumName := patterns[0].enumName
decl, ok := resolver.program.Enums[enumName]
if !ok {
resolver.fail(fmt.Errorf("match value is not a known enum"))
return
}
seen := map[string]bool{}
for _, pattern := range patterns {
if pattern.enumName != enumName {
resolver.fail(fmt.Errorf("match case %s.%s does not match enum %s", pattern.enumName, pattern.variantName, enumName))
return
}
if seen[pattern.variantName] {
resolver.fail(fmt.Errorf("duplicate match case %s.%s", enumName, pattern.variantName))
return
}
seen[pattern.variantName] = true
variant := enumVariant(decl, pattern.variantName)
if variant == nil {
resolver.fail(fmt.Errorf("unknown variant %s.%s", enumName, pattern.variantName))
return
}
if len(pattern.bindings) != len(variant.PayloadTypes) {
resolver.fail(fmt.Errorf("match case %s.%s expects %d bindings", enumName, pattern.variantName, len(variant.PayloadTypes)))
return
}
}
for _, variant := range decl.Variants {
if !seen[variant.Name] {
resolver.fail(fmt.Errorf("non-exhaustive match for %s: missing %s", enumName, variant.Name))
return
}
}
}
func (resolver *semanticResolver) fail(err error) {
if resolver.err == nil {
resolver.err = err
}
}
func (resolver *semanticResolver) validateEnumExpression(expr Expr, meaning ExprMeaning) {
if meaning != EnumConstructionExpr {
return
}
var enumName, variantName string
valueCount := 0
switch value := expr.(type) {
case CallExpr:
selector, ok := value.Callee.(SelectorExpr)
if !ok {
return
}
receiver, ok := selector.Receiver.(IdentExpr)
if !ok {
return
}
enumName, variantName, valueCount = receiver.Name, selector.Name, len(value.Args)
case SelectorExpr:
receiver, ok := value.Receiver.(IdentExpr)
if !ok {
return
}
enumName, variantName = receiver.Name, value.Name
default:
return
}
if enumName == "Result" {
if valueCount != 1 {
resolver.fail(fmt.Errorf("Result.%s expects one value", variantName))
}
return
}
decl, ok := resolver.program.Enums[enumName]
if !ok {
return
}
variant := enumVariant(decl, variantName)
if variant == nil {
resolver.fail(fmt.Errorf("unknown variant %s.%s", enumName, variantName))
return
}
if len(variant.PayloadTypes) != valueCount {
resolver.fail(fmt.Errorf("variant %s.%s expects %d values", enumName, variantName, len(variant.PayloadTypes)))
}
}
func collectionElement(typ Type) Type {
if generic, ok := typ.(GenericType); ok && len(generic.Args) > 0 {
return generic.Args[len(generic.Args)-1]
}
return UnknownType{}
}
func functionResult(typ Type) Type {
if function, ok := typ.(FunctionType); ok {
return function.Result
}
return UnknownType{}
}
func isResultType(typ Type) bool {
result, ok := typ.(GenericType)
return ok && result.Base.String() == "Result" && len(result.Args) == 2
}

View file

@ -13,23 +13,20 @@ type mappingState struct {
}
func (g *goGenerator) mappingTopLevelTarget(target string) string {
if _, ok := g.classForType(target); ok && !strings.HasPrefix(target, "*") {
return "*" + target
}
return target
}
func (g *goGenerator) ensureMapping(source, target, path string) (string, error) {
key := source + "->" + target
if function, ok := g.mappings.functions[key]; ok {
if function, ok := g.semantic.Mappings.functions[key]; ok {
return function, nil
}
if err := g.validateMapping(source, target, path, map[string]bool{}); err != nil {
return "", err
}
function := fmt.Sprintf("gotlinMap%d", len(g.mappings.pairs)+1)
g.mappings.functions[key] = function
g.mappings.pairs = append(g.mappings.pairs, mappingPair{source: source, target: target, function: function})
function := fmt.Sprintf("gotlinMap%d", len(g.semantic.Mappings.pairs)+1)
g.semantic.Mappings.functions[key] = function
g.semantic.Mappings.pairs = append(g.semantic.Mappings.pairs, mappingPair{source: source, target: target, function: function})
return function, nil
}
@ -83,8 +80,8 @@ func (g *goGenerator) validateMapping(source, target, path string, seen map[stri
}
return nil
}
sourceEnum, sourceEnumOK := g.enums[strings.TrimPrefix(source, "*")]
targetEnum, targetEnumOK := g.enums[strings.TrimPrefix(target, "*")]
sourceEnum, sourceEnumOK := g.semantic.Enums[strings.TrimPrefix(source, "*")]
targetEnum, targetEnumOK := g.semantic.Enums[strings.TrimPrefix(target, "*")]
if sourceEnumOK || targetEnumOK {
if sourceEnumOK && enumIsString(sourceEnum) && target == "String" {
return nil
@ -134,16 +131,16 @@ func mappingFieldName(class ClassDecl, field FieldDecl) string {
}
func (g *goGenerator) emitMapping(pair mappingPair) error {
g.line(fmt.Sprintf("func %s(source %s) %s {", pair.function, mapGoType(pair.source), mapGoType(pair.target)))
g.line(fmt.Sprintf("func %s(source %s) %s {", pair.function, g.goType(pair.source), g.goType(pair.target)))
g.indentLevel++
if sourceEnum, ok := g.enums[strings.TrimPrefix(pair.source, "*")]; ok {
if sourceEnum, ok := g.semantic.Enums[strings.TrimPrefix(pair.source, "*")]; ok {
if enumIsString(sourceEnum) && pair.target == "String" {
g.line("return string(source)")
g.indentLevel--
g.line("}")
return nil
}
targetEnum := g.enums[strings.TrimPrefix(pair.target, "*")]
targetEnum := g.semantic.Enums[strings.TrimPrefix(pair.target, "*")]
if enumIsString(sourceEnum) && enumIsString(targetEnum) {
g.line("return " + targetEnum.Name + "(source)")
g.indentLevel--
@ -170,7 +167,7 @@ func (g *goGenerator) emitMapping(pair mappingPair) error {
g.indentLevel--
g.line("}")
g.line(`panic("unreachable enum mapping")`)
} else if targetEnum, ok := g.enums[strings.TrimPrefix(pair.target, "*")]; ok && pair.source == "String" && enumIsString(targetEnum) {
} else if targetEnum, ok := g.semantic.Enums[strings.TrimPrefix(pair.target, "*")]; ok && pair.source == "String" && enumIsString(targetEnum) {
g.line("switch source {")
g.indentLevel++
for _, variant := range targetEnum.Variants {
@ -201,18 +198,22 @@ func (g *goGenerator) mappingExpr(expr, source, target, path string) (string, er
if strings.HasSuffix(source, "?") || strings.HasSuffix(target, "?") {
sourceInner := strings.TrimSuffix(source, "?")
targetInner := strings.TrimSuffix(target, "?")
inner, err := g.mappingExpr("*value", sourceInner, targetInner, path)
innerExpr := "*value"
if _, class := g.classForType(sourceInner); class {
innerExpr = "value"
}
inner, err := g.mappingExpr(innerExpr, sourceInner, targetInner, path)
if err != nil {
return "", err
}
if strings.HasSuffix(source, "?") {
return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; mapped := %s; return &mapped }(%s)", mapGoType(source), mapGoType(target), inner, expr), nil
return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; return %s }(%s)", g.goType(source), g.goType(target), inner, expr), nil
}
inner, err = g.mappingExpr("value", sourceInner, targetInner, path)
if err != nil {
return "", err
}
return fmt.Sprintf("func(value %s) %s { mapped := %s; return &mapped }(%s)", mapGoType(source), mapGoType(target), inner, expr), nil
return fmt.Sprintf("func(value %s) %s { return %s }(%s)", g.goType(source), g.goType(target), inner, expr), nil
}
if sourceBase, sourceArgs, ok := parseGenericType(source); ok {
_, targetArgs, _ := parseGenericType(target)
@ -221,14 +222,14 @@ func (g *goGenerator) mappingExpr(expr, source, target, path string) (string, er
if err != nil {
return "", err
}
return fmt.Sprintf("func(values %s) %s { var result %s; for _, item := range values { result = append(result, %s) }; return result }(%s)", mapGoType(source), mapGoType(target), mapGoType(target), item, expr), nil
return fmt.Sprintf("func(values %s) %s { var result %s; for _, item := range values { result = append(result, %s) }; return result }(%s)", g.goType(source), g.goType(target), g.goType(target), item, expr), nil
}
if sourceBase == "Map" || sourceBase == "MutableMap" {
value, err := g.mappingExpr("item", sourceArgs[1], targetArgs[1], path+"[]")
if err != nil {
return "", err
}
return fmt.Sprintf("func(values %s) %s { result := make(%s, len(values)); for key, item := range values { result[key] = %s }; return result }(%s)", mapGoType(source), mapGoType(target), mapGoType(target), value, expr), nil
return fmt.Sprintf("func(values %s) %s { result := make(%s, len(values)); for key, item := range values { result[key] = %s }; return result }(%s)", g.goType(source), g.goType(target), g.goType(target), value, expr), nil
}
}
if sourceClass, ok := g.classForType(source); ok {
@ -242,23 +243,16 @@ func (g *goGenerator) mappingExpr(expr, source, target, path string) (string, er
}
fields = append(fields, mappingFieldName(targetClass, targetField)+": "+mapped)
}
literal := targetClass.Name + "{" + strings.Join(fields, ", ") + "}"
if strings.HasPrefix(target, "*") {
literal = "&" + literal
}
if strings.HasPrefix(source, "*") {
return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; return %s }(%s)", mapGoType(source), mapGoType(target), strings.ReplaceAll(literal, expr+".", "value."), expr), nil
}
return literal, nil
return "&" + targetClass.Name + "{" + strings.Join(fields, ", ") + "}", nil
}
if _, ok := g.enums[strings.TrimPrefix(source, "*")]; ok {
if _, ok := g.semantic.Enums[strings.TrimPrefix(source, "*")]; ok {
function, err := g.ensureMapping(source, target, path)
if err != nil {
return "", err
}
return function + "(" + expr + ")", nil
}
if targetEnum, ok := g.enums[strings.TrimPrefix(target, "*")]; ok && source == "String" && enumIsString(targetEnum) {
if targetEnum, ok := g.semantic.Enums[strings.TrimPrefix(target, "*")]; ok && source == "String" && enumIsString(targetEnum) {
function, err := g.ensureMapping(source, target, path)
if err != nil {
return "", err

View file

@ -10,9 +10,9 @@ func TestGenerateRecursiveClassMapping(t *testing.T) {
package demo
data class SourceAddress(var city: String)
data class TargetAddress(var city: String)
data class Source(var id: String, var address: *SourceAddress)
data class Target(var address: *TargetAddress, var id: String)
fun convert(source: *Source): *Target { return source.mapTo<Target>() }
data class Source(var id: String, var address: SourceAddress)
data class Target(var address: TargetAddress, var id: String)
fun convert(source: Source): Target { return source.mapTo<Target>() }
`)
if err != nil {
t.Fatal(err)
@ -22,7 +22,7 @@ fun convert(source: *Source): *Target { return source.mapTo<Target>() }
t.Fatal(err)
}
code := string(out)
for _, want := range []string{"gotlinMap1(source)", "Address:", "Id: value.Id", "TargetAddress{City: value.City}"} {
for _, want := range []string{"gotlinMap1(source)", "Address:", "Id: source.Id", "&TargetAddress{City: source.Address.City}"} {
if !strings.Contains(code, want) {
t.Fatalf("missing %q:\n%s", want, code)
}
@ -34,8 +34,8 @@ func TestGenerateListAndMapMapping(t *testing.T) {
package demo
data class Source(var id: String)
data class Target(var id: String)
fun list(values: List<*Source>): List<*Target> { return values.mapTo<List<*Target>>() }
fun mapping(values: Map<String, *Source>): Map<String, *Target> { return values.mapTo<Map<String, *Target>>() }
fun list(values: List<Source>): List<Target> { return values.mapTo<List<Target>>() }
fun mapping(values: Map<String, Source>): Map<String, Target> { return values.mapTo<Map<String, Target>>() }
`)
if err != nil {
t.Fatal(err)
@ -77,9 +77,9 @@ func TestMappingReportsNestedFieldPath(t *testing.T) {
package demo
data class SourceAddress(var zip: String)
data class TargetAddress(var zip: Int)
data class Source(var address: *SourceAddress)
data class Target(var address: *TargetAddress)
fun convert(value: *Source): *Target { return value.mapTo<Target>() }
data class Source(var address: SourceAddress)
data class Target(var address: TargetAddress)
fun convert(value: Source): Target { return value.mapTo<Target>() }
`)
if err != nil {
t.Fatal(err)
@ -92,7 +92,7 @@ fun convert(value: *Source): *Target { return value.mapTo<Target>() }
func TestMappingRejectsMissingFieldAndEnumVariant(t *testing.T) {
for _, source := range []string{
`package demo data class Source(var id: String) data class Target(var id: String, var name: String) fun convert(value: *Source): *Target { return value.mapTo<Target>() }`,
`package demo data class Source(var id: String) data class Target(var id: String, var name: String) fun convert(value: Source): Target { return value.mapTo<Target>() }`,
`package demo enum Source { Ready, Failed } enum Target { Ready } fun convert(value: Source): Target { return value.mapTo<Target>() }`,
} {
prog, err := Parse(source)
@ -110,8 +110,8 @@ func TestMapStringBackedEnumToAndFromString(t *testing.T) {
enum Status { PendingReservation, Initiated }
data class Domain(var status: Status)
data class Row(var status: String)
fun toRow(value: *Domain): *Row { return value.mapTo<Row>() }
fun toDomain(value: *Row): *Domain { return value.mapTo<Domain>() }`)
fun toRow(value: Domain): Row { return value.mapTo<Row>() }
fun toDomain(value: Row): Domain { return value.mapTo<Domain>() }`)
if err != nil {
t.Fatal(err)
}
@ -130,10 +130,10 @@ func TestMapToInfersExpectedTargetType(t *testing.T) {
prog, err := Parse(`package demo
data class Source(var id: String)
data class Target(var id: String)
data class Wrapper(var target: *Target)
fun returned(value: *Source): *Target { return value.mapTo() }
fun wrapped(value: *Source): *Wrapper { return Wrapper(value.mapTo()) }
fun local(value: *Source): *Target { val target: *Target = value.mapTo(); return target }`)
data class Wrapper(var target: Target)
fun returned(value: Source): Target { return value.mapTo() }
fun wrapped(value: Source): Wrapper { return Wrapper(value.mapTo()) }
fun local(value: Source): Target { val target: Target = value.mapTo(); return target }`)
if err != nil {
t.Fatal(err)
}
@ -148,7 +148,7 @@ fun local(value: *Source): *Target { val target: *Target = value.mapTo(); return
func TestMapToWithoutTargetContextHasHelpfulError(t *testing.T) {
prog, err := Parse(`package demo
fun convert(value: *Source) { val target = value.mapTo() }`)
fun convert(value: Source) { val target = value.mapTo() }`)
if err != nil {
t.Fatal(err)
}

View file

@ -13,7 +13,7 @@ class Repository {
fun healthy(): Boolean { return true }
}
class Service(val repository: *Repository) {
class Service(val repository: Repository) {
fun healthy(): Boolean { return repository.healthy() }
}
@ -34,9 +34,9 @@ func TestInternalMethodReturnTypeFlowsThroughSelectors(t *testing.T) {
prog, err := Parse(`
package demo
class Transaction { fun commit(): Boolean { return true } }
class Result(val transaction: *Transaction)
class Repository { fun begin(): *Result { return Result(Transaction()) } }
class Service(val repository: *Repository) {
class Result(val transaction: Transaction)
class Repository { fun begin(): Result { return Result(Transaction()) } }
class Service(val repository: Repository) {
fun run(): Boolean { return repository.begin().transaction.commit() }
}
`)

View file

@ -8,8 +8,8 @@ import (
func TestSafeAccessAndNonNullAssertion(t *testing.T) {
prog, err := Parse(`package demo
data class User(var email: String)
fun safe(user: *User?): String? { return user?.email }
fun required(user: *User?): String { return user!!.email }`)
fun safe(user: User?): String? { return user?.email }
fun required(user: User?): String { return user!!.email }`)
if err != nil {
t.Fatal(err)
}
@ -26,7 +26,7 @@ fun required(user: *User?): String { return user!!.email }`)
func TestRejectNullableDereference(t *testing.T) {
prog, err := Parse(`package demo
fun unsafe(user: *User?): String { return user.email }`)
fun unsafe(user: User?): String { return user.email }`)
if err != nil {
t.Fatal(err)
}
@ -38,8 +38,8 @@ fun unsafe(user: *User?): String { return user.email }`)
func TestRejectNullForNonNullableTypes(t *testing.T) {
for _, source := range []string{
`package demo data class User(var email: String) fun main() { val user: *User = null }`,
`package demo data class User(var email: String) fun use(user: *User) {} fun main() { use(null) }`,
`package demo data class User(var email: String) fun main() { val user: User = null }`,
`package demo data class User(var email: String) fun use(user: User) {} fun main() { use(null) }`,
`package demo fun name(): String { return null }`,
`package demo fun main() { val value = null }`,
} {
@ -56,8 +56,8 @@ func TestRejectNullForNonNullableTypes(t *testing.T) {
func TestNullableSmartCasts(t *testing.T) {
prog, err := Parse(`package demo
data class User(var email: String)
fun guarded(user: *User?): String { if (user == null) { return "missing" }; return user.email }
fun branched(user: *User?): String { if (user != null) { return user.email } else { return "missing" } }`)
fun guarded(user: User?): String { if (user == null) { return "missing" }; return user.email }
fun branched(user: User?): String { if (user != null) { return user.email } else { return "missing" } }`)
if err != nil {
t.Fatal(err)
}

View file

@ -131,12 +131,6 @@ func (p *parser) parseProgram() (*Program, error) {
default:
return nil, fmt.Errorf("unsupported annotation %q", annotation.lexeme)
}
case p.check(tokenWorker):
decl, err := p.parseWorker()
if err != nil {
return nil, err
}
prog.Workers = append(prog.Workers, decl)
case p.check(tokenFun) || p.check(tokenSuspend):
fn, err := p.parseFunction()
if err != nil {
@ -338,68 +332,6 @@ func (p *parser) parseClass() (ClassDecl, error) {
return ClassDecl{Name: name.lexeme, Data: data, Fields: fields, Parents: parents, Methods: methods}, nil
}
func (p *parser) parseWorker() (WorkerDecl, error) {
if _, err := p.expect(tokenWorker, "expected 'worker'"); err != nil {
return WorkerDecl{}, err
}
name, err := p.expect(tokenIdent, "expected worker name")
if err != nil {
return WorkerDecl{}, err
}
if _, err := p.expect(tokenLBrace, "expected '{'"); err != nil {
return WorkerDecl{}, err
}
var fields []WorkerFieldDecl
var methods []FunctionDecl
for !p.check(tokenRBrace) && !p.check(tokenEOF) {
switch {
case p.match(tokenVal), p.match(tokenVar):
mutable := p.tokens[p.pos-1].kind == tokenVar
fieldName, err := p.expect(tokenIdent, "expected field name")
if err != nil {
return WorkerDecl{}, err
}
typ := ""
if p.match(tokenColon) {
parsed, err := p.parseTypeRef()
if err != nil {
return WorkerDecl{}, err
}
typ = parsed
}
if _, err := p.expect(tokenAssign, "expected '=' after field declaration"); err != nil {
return WorkerDecl{}, err
}
value, err := p.parseExpr(0)
if err != nil {
return WorkerDecl{}, err
}
fields = append(fields, WorkerFieldDecl{
Mutable: mutable,
Name: fieldName.lexeme,
Type: typ,
Value: value,
})
p.match(tokenSemicolon)
case p.check(tokenFun):
method, err := p.parseFunction()
if err != nil {
return WorkerDecl{}, err
}
methods = append(methods, method)
p.match(tokenSemicolon)
default:
tok := p.peek()
return WorkerDecl{}, fmt.Errorf("expected worker member at %d, found %q", tok.pos, tok.lexeme)
}
}
if _, err := p.expect(tokenRBrace, "expected '}'"); err != nil {
return WorkerDecl{}, err
}
return WorkerDecl{Name: name.lexeme, Fields: fields, Methods: methods}, nil
}
func (p *parser) parseClassParents() ([]string, error) {
if !p.match(tokenColon) {
return nil, nil
@ -626,6 +558,9 @@ func (p *parser) parseStmt() (Stmt, error) {
if p.check(tokenIdent) && p.peek().lexeme == "go" {
return nil, fmt.Errorf("bare go is removed; use launch inside a coroutine scope")
}
if p.check(tokenIdent) && p.peek().lexeme == "select" {
return nil, fmt.Errorf("select is removed; use structured coroutines and channel read()")
}
switch {
case p.match(tokenVal):
return p.parseVarDecl(false)
@ -646,25 +581,6 @@ func (p *parser) parseStmt() (Stmt, error) {
return nil, err
}
return ThrowStmt{Value: expr}, nil
case p.match(tokenGo):
var expr Expr
var err error
if p.check(tokenLBrace) {
body, err := p.parseBlock()
if err != nil {
return nil, err
}
expr = CallExpr{Callee: LambdaExpr{Body: body}}
} else {
expr, err = p.parseExpr(0)
if err != nil {
return nil, err
}
}
if _, ok := expr.(CallExpr); !ok {
return nil, fmt.Errorf("'go' expects a function call expression")
}
return GoStmt{Value: expr}, nil
case p.match(tokenDefer):
expr, err := p.parseExpr(0)
if err != nil {
@ -680,8 +596,6 @@ func (p *parser) parseStmt() (Stmt, error) {
return p.parseWhile()
case p.match(tokenFor):
return p.parseForEach()
case p.match(tokenSelect):
return p.parseSelect()
case p.match(tokenMatch):
return p.parseMatch()
case p.match(tokenTry):
@ -855,41 +769,6 @@ func (p *parser) parseMatchExpr() (Expr, error) {
return MatchExpr{Value: value, Cases: cases}, nil
}
func (p *parser) parseSelect() (Stmt, error) {
if _, err := p.expect(tokenLBrace, "expected '{' after select"); err != nil {
return nil, err
}
var cases []SelectCase
for !p.check(tokenRBrace) && !p.check(tokenEOF) {
source, err := p.parseExpr(0)
if err != nil {
return nil, err
}
if _, err := p.expect(tokenArrow, "expected '->' in select case"); err != nil {
return nil, err
}
var body []Stmt
if p.check(tokenLBrace) {
body, err = p.parseBlock()
if err != nil {
return nil, err
}
} else {
stmt, err := p.parseStmt()
if err != nil {
return nil, err
}
body = []Stmt{stmt}
p.match(tokenSemicolon)
}
cases = append(cases, SelectCase{Source: source, Body: body})
}
if _, err := p.expect(tokenRBrace, "expected '}' after select"); err != nil {
return nil, err
}
return SelectStmt{Cases: cases}, nil
}
func (p *parser) parseTryCatch() (Stmt, error) {
tryBody, err := p.parseBlock()
if err != nil {

View file

@ -1,197 +0,0 @@
package lang
import "strings"
func normalizeClassReferences(program *Program) {
classes := map[string]bool{}
for _, class := range program.Classes {
classes[class.Name] = true
}
normalize := func(value string) string { return normalizeReferenceType(value, classes) }
for i := range program.Interfaces {
for j := range program.Interfaces[i].Methods {
normalizeSignature(&program.Interfaces[i].Methods[j], normalize)
}
}
for i := range program.Enums {
for j := range program.Enums[i].Variants {
for k := range program.Enums[i].Variants[j].PayloadTypes {
program.Enums[i].Variants[j].PayloadTypes[k] = normalize(program.Enums[i].Variants[j].PayloadTypes[k])
}
}
}
for i := range program.Classes {
for j := range program.Classes[i].Fields {
program.Classes[i].Fields[j].Type = normalize(program.Classes[i].Fields[j].Type)
}
for j := range program.Classes[i].Methods {
normalizeFunction(&program.Classes[i].Methods[j], normalize)
}
}
for i := range program.Workers {
for j := range program.Workers[i].Fields {
program.Workers[i].Fields[j].Type = normalize(program.Workers[i].Fields[j].Type)
normalizeExprTypes(program.Workers[i].Fields[j].Value, normalize)
}
for j := range program.Workers[i].Methods {
normalizeFunction(&program.Workers[i].Methods[j], normalize)
}
}
for i := range program.Functions {
normalizeFunction(&program.Functions[i], normalize)
}
}
func normalizeSignature(signature *FunctionSignature, normalize func(string) string) {
for i := range signature.Params {
signature.Params[i].Type = normalize(signature.Params[i].Type)
}
signature.ReturnType = normalize(signature.ReturnType)
}
func normalizeFunction(function *FunctionDecl, normalize func(string) string) {
for i := range function.Params {
function.Params[i].Type = normalize(function.Params[i].Type)
}
function.ReturnType = normalize(function.ReturnType)
normalizeStmtTypes(function.Body, normalize)
}
func normalizeStmtTypes(statements []Stmt, normalize func(string) string) {
for index, statement := range statements {
switch value := statement.(type) {
case VarDecl:
value.Type = normalize(value.Type)
normalizeExprTypes(value.Value, normalize)
statements[index] = value
case MultiVarDecl:
normalizeExprTypes(value.Value, normalize)
case AssignStmt:
normalizeExprTypes(value.Value, normalize)
case AddAssignStmt:
normalizeExprTypes(value.Value, normalize)
case MultiAssignStmt:
normalizeExprTypes(value.Value, normalize)
case ReturnStmt:
if value.Value != nil {
normalizeExprTypes(value.Value, normalize)
}
case ThrowStmt:
normalizeExprTypes(value.Value, normalize)
case GoStmt:
normalizeExprTypes(value.Value, normalize)
case DeferStmt:
normalizeExprTypes(value.Value, normalize)
case ExprStmt:
normalizeExprTypes(value.Value, normalize)
case IfStmt:
normalizeExprTypes(value.Cond, normalize)
normalizeStmtTypes(value.Then, normalize)
normalizeStmtTypes(value.Else, normalize)
case WhileStmt:
normalizeExprTypes(value.Cond, normalize)
normalizeStmtTypes(value.Body, normalize)
case ForEachStmt:
normalizeExprTypes(value.Source, normalize)
normalizeStmtTypes(value.Body, normalize)
case SelectStmt:
for _, c := range value.Cases {
normalizeExprTypes(c.Source, normalize)
normalizeStmtTypes(c.Body, normalize)
}
case MatchStmt:
normalizeExprTypes(value.Value, normalize)
for _, c := range value.Cases {
normalizeStmtTypes(c.Body, normalize)
}
case TryCatchStmt:
value.CatchType = normalize(value.CatchType)
normalizeStmtTypes(value.TryBody, normalize)
normalizeStmtTypes(value.CatchBody, normalize)
statements[index] = value
}
}
}
func normalizeExprTypes(expression Expr, normalize func(string) string) {
switch value := expression.(type) {
case UnaryExpr:
normalizeExprTypes(value.Value, normalize)
case NonNullExpr:
normalizeExprTypes(value.Value, normalize)
case BinaryExpr:
normalizeExprTypes(value.Left, normalize)
normalizeExprTypes(value.Right, normalize)
case SelectorExpr:
normalizeExprTypes(value.Receiver, normalize)
case SafeSelectorExpr:
normalizeExprTypes(value.Receiver, normalize)
case IndexExpr:
normalizeExprTypes(value.Receiver, normalize)
normalizeExprTypes(value.Index, normalize)
case EnumVariantExpr:
for _, item := range value.Values {
normalizeExprTypes(item, normalize)
}
case MatchExpr:
normalizeExprTypes(value.Value, normalize)
for _, item := range value.Cases {
normalizeExprTypes(item.Value, normalize)
}
case LambdaExpr:
for i := range value.Params {
value.Params[i].Type = normalize(value.Params[i].Type)
}
normalizeStmtTypes(value.Body, normalize)
case CallExpr:
skipTypeArgs := false
if selector, ok := value.Callee.(SelectorExpr); ok {
if root, ok := selector.Receiver.(IdentExpr); ok && root.Name == "sql" {
skipTypeArgs = true
}
}
if !skipTypeArgs {
for i := range value.TypeArgs {
value.TypeArgs[i] = normalize(value.TypeArgs[i])
}
}
normalizeExprTypes(value.Callee, normalize)
for _, item := range value.Args {
normalizeExprTypes(item, normalize)
}
for _, item := range value.NamedArgs {
normalizeExprTypes(item.Value, normalize)
}
}
}
func normalizeReferenceType(value string, classes map[string]bool) string {
if value == "" {
return value
}
nullable := strings.HasSuffix(value, "?")
if nullable {
value = strings.TrimSuffix(value, "?")
}
explicitPointer := strings.HasPrefix(value, "*")
if explicitPointer {
value = strings.TrimPrefix(value, "*")
}
if params, result, ok := parseFunctionType(value); ok {
for i := range params {
params[i] = normalizeReferenceType(params[i], classes)
}
value = "(" + strings.Join(params, ", ") + ") -> " + normalizeReferenceType(result, classes)
} else if base, args, ok := parseGenericType(value); ok {
for i := range args {
args[i] = normalizeReferenceType(args[i], classes)
}
value = base + "<" + strings.Join(args, ", ") + ">"
} else if classes[value] || explicitPointer {
value = "*" + value
}
if nullable {
value += "?"
}
return value
}

View file

@ -26,18 +26,15 @@ fun optional(): Repository? { return null }`)
}
}
func TestExplicitClassPointerRemainsCompatible(t *testing.T) {
func TestExplicitClassPointerIsRejected(t *testing.T) {
prog, err := Parse(`package demo
class Repository
fun use(repository: *Repository): *Repository { return repository }`)
if err != nil {
t.Fatal(err)
}
out, err := GenerateGo(prog)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(out), "**Repository") || !strings.Contains(string(out), "repository *Repository") {
t.Fatalf("unexpected explicit pointer output:\n%s", out)
_, err = GenerateGo(prog)
if err == nil || !strings.Contains(err.Error(), "already reference-valued") {
t.Fatalf("unexpected explicit pointer result: %v", err)
}
}

View file

@ -2,31 +2,17 @@ package lang
import "fmt"
func validateMutability(program *Program) error {
checker := mutabilityChecker{}
for _, fn := range program.Functions {
if err := checker.checkFunction(fn, nil, nil); err != nil {
func validateMutability(semantic *SemanticProgram) error {
checker := mutabilityChecker{semantic: semantic}
for index := range semantic.Syntax.Functions {
if err := checker.checkFunction(&semantic.Syntax.Functions[index], nil); err != nil {
return err
}
}
for _, class := range program.Classes {
fields := make(map[string]bool, len(class.Fields))
for _, field := range class.Fields {
fields[field.Name] = field.Mutable
}
for _, method := range class.Methods {
if err := checker.checkFunction(method, fields, nil); err != nil {
return err
}
}
}
for _, worker := range program.Workers {
fields := make(map[string]bool, len(worker.Fields))
for _, field := range worker.Fields {
fields[field.Name] = field.Mutable
}
for _, method := range worker.Methods {
if err := checker.checkFunction(method, nil, fields); err != nil {
for index := range semantic.Syntax.Classes {
class := semantic.ClassInfo[semantic.Syntax.Classes[index].Name]
for methodIndex := range semantic.Syntax.Classes[index].Methods {
if err := checker.checkFunction(&semantic.Syntax.Classes[index].Methods[methodIndex], class); err != nil {
return err
}
}
@ -35,138 +21,123 @@ func validateMutability(program *Program) error {
}
type mutabilityChecker struct {
scopes []map[string]bool
classFields map[string]bool
workerFields map[string]bool
semantic *SemanticProgram
scope *Scope
class *ClassSymbol
}
func (c *mutabilityChecker) checkFunction(fn FunctionDecl, classFields map[string]bool, workerFields map[string]bool) error {
c.scopes = nil
c.classFields = classFields
c.workerFields = workerFields
c.pushScope()
defer c.popScope()
for _, param := range fn.Params {
c.define(param.Name, false)
func (checker *mutabilityChecker) checkFunction(function *FunctionDecl, class *ClassSymbol) error {
checker.class = class
checker.scope = NewScope(checker.semantic.Global)
if class != nil {
_ = checker.scope.Define(&Symbol{Name: "this", Kind: VariableSymbol, Type: ClassType{Class: class}})
}
return c.checkStmts(fn.Body)
for _, param := range function.Params {
typ, _ := checker.semantic.ResolveType(param.Type)
_ = checker.scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
}
return checker.checkStmts(function.Body)
}
func (c *mutabilityChecker) checkStmts(stmts []Stmt) error {
for _, stmt := range stmts {
switch s := stmt.(type) {
func (checker *mutabilityChecker) checkStmts(statements []Stmt) error {
for _, statement := range statements {
switch value := statement.(type) {
case VarDecl:
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
c.define(s.Name, s.Mutable)
typ, _ := checker.semantic.ResolveType(value.Type)
_ = checker.scope.Define(&Symbol{Name: value.Name, Kind: VariableSymbol, Type: typ, Mutable: value.Mutable})
case MultiVarDecl:
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
for _, name := range s.Names {
c.define(name, s.Mutable)
for _, name := range value.Names {
_ = checker.scope.Define(&Symbol{Name: name, Kind: VariableSymbol, Type: UnknownType{}, Mutable: value.Mutable})
}
case AssignStmt:
if err := c.requireMutable(s.Name, s.Pos); err != nil {
if err := checker.requireMutable(value.Name, value.Pos); err != nil {
return err
}
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
case AddAssignStmt:
if err := c.requireMutable(s.Name, s.Pos); err != nil {
if err := checker.requireMutable(value.Name, value.Pos); err != nil {
return err
}
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
case MultiAssignStmt:
for i, name := range s.Names {
pos := 0
if i < len(s.Positions) {
pos = s.Positions[i]
for index, name := range value.Names {
position := 0
if index < len(value.Positions) {
position = value.Positions[index]
}
if err := c.requireMutable(name, pos); err != nil {
if err := checker.requireMutable(name, position); err != nil {
return err
}
}
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
case ReturnStmt:
if s.Value != nil {
if err := c.checkExpr(s.Value); err != nil {
if value.Value != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
}
case ThrowStmt:
if err := c.checkExpr(s.Value); err != nil {
return err
}
case GoStmt:
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
case DeferStmt:
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
case ExprStmt:
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
case IfStmt:
if err := c.checkExpr(s.Cond); err != nil {
if err := checker.checkExpr(value.Cond); err != nil {
return err
}
if err := c.checkBlock(s.Then, nil); err != nil {
if err := checker.checkBlock(value.Then, nil); err != nil {
return err
}
if err := c.checkBlock(s.Else, nil); err != nil {
if err := checker.checkBlock(value.Else, nil); err != nil {
return err
}
case WhileStmt:
if err := c.checkExpr(s.Cond); err != nil {
if err := checker.checkExpr(value.Cond); err != nil {
return err
}
if err := c.checkBlock(s.Body, nil); err != nil {
if err := checker.checkBlock(value.Body, nil); err != nil {
return err
}
case ForEachStmt:
if err := c.checkExpr(s.Source); err != nil {
if err := checker.checkExpr(value.Source); err != nil {
return err
}
if err := c.checkBlock(s.Body, map[string]bool{s.Name: false}); err != nil {
if err := checker.checkBlock(value.Body, []string{value.Name}); err != nil {
return err
}
case SelectStmt:
for _, sc := range s.Cases {
if err := c.checkExpr(sc.Source); err != nil {
return err
}
if err := c.checkBlock(sc.Body, map[string]bool{"it": false}); err != nil {
return err
}
}
case MatchStmt:
if err := c.checkExpr(s.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
for _, matchCase := range s.Cases {
bindings := map[string]bool{}
for _, binding := range matchCase.Bindings {
bindings[binding] = false
}
if err := c.checkBlock(matchCase.Body, bindings); err != nil {
for _, matchCase := range value.Cases {
if err := checker.checkBlock(matchCase.Body, matchCase.Bindings); err != nil {
return err
}
}
case TryCatchStmt:
if err := c.checkBlock(s.TryBody, nil); err != nil {
if err := checker.checkBlock(value.TryBody, nil); err != nil {
return err
}
if err := c.checkBlock(s.CatchBody, map[string]bool{s.CatchName: false}); err != nil {
if err := checker.checkBlock(value.CatchBody, []string{value.CatchName}); err != nil {
return err
}
}
@ -174,124 +145,104 @@ func (c *mutabilityChecker) checkStmts(stmts []Stmt) error {
return nil
}
func (c *mutabilityChecker) checkBlock(stmts []Stmt, bindings map[string]bool) error {
c.pushScope()
defer c.popScope()
for name, mutable := range bindings {
c.define(name, mutable)
func (checker *mutabilityChecker) checkBlock(statements []Stmt, bindings []string) error {
previous := checker.scope
checker.scope = NewScope(previous)
defer func() { checker.scope = previous }()
for _, name := range bindings {
_ = checker.scope.Define(&Symbol{Name: name, Kind: VariableSymbol, Type: UnknownType{}})
}
return c.checkStmts(stmts)
return checker.checkStmts(statements)
}
func (c *mutabilityChecker) checkExpr(expr Expr) error {
switch e := expr.(type) {
func (checker *mutabilityChecker) checkExpr(expr Expr) error {
switch value := expr.(type) {
case UnaryExpr:
return c.checkExpr(e.Value)
return checker.checkExpr(value.Value)
case BinaryExpr:
if err := c.checkExpr(e.Left); err != nil {
if err := checker.checkExpr(value.Left); err != nil {
return err
}
return c.checkExpr(e.Right)
return checker.checkExpr(value.Right)
case CallExpr:
if err := c.checkExpr(e.Callee); err != nil {
if err := checker.checkExpr(value.Callee); err != nil {
return err
}
for _, arg := range e.Args {
if err := c.checkExpr(arg); err != nil {
for _, argument := range value.Args {
if err := checker.checkExpr(argument); err != nil {
return err
}
}
for _, arg := range e.NamedArgs {
if err := c.checkExpr(arg.Value); err != nil {
for _, argument := range value.NamedArgs {
if err := checker.checkExpr(argument.Value); err != nil {
return err
}
}
case SelectorExpr:
return c.checkExpr(e.Receiver)
return checker.checkExpr(value.Receiver)
case SafeSelectorExpr:
return c.checkExpr(e.Receiver)
return checker.checkExpr(value.Receiver)
case NonNullExpr:
return c.checkExpr(e.Value)
return checker.checkExpr(value.Value)
case TryExpr:
return c.checkExpr(e.Value)
return checker.checkExpr(value.Value)
case IndexExpr:
if err := c.checkExpr(e.Receiver); err != nil {
if err := checker.checkExpr(value.Receiver); err != nil {
return err
}
return c.checkExpr(e.Index)
return checker.checkExpr(value.Index)
case EnumVariantExpr:
for _, value := range e.Values {
if err := c.checkExpr(value); err != nil {
for _, item := range value.Values {
if err := checker.checkExpr(item); err != nil {
return err
}
}
case MatchExpr:
if err := c.checkExpr(e.Value); err != nil {
if err := checker.checkExpr(value.Value); err != nil {
return err
}
for _, matchCase := range e.Cases {
if err := c.checkExpr(matchCase.Value); err != nil {
for _, matchCase := range value.Cases {
if err := checker.checkExpr(matchCase.Value); err != nil {
return err
}
}
case LambdaExpr:
bindings := map[string]bool{}
if e.ImplicitIt {
bindings["it"] = false
previous := checker.scope
checker.scope = NewScope(previous)
defer func() { checker.scope = previous }()
if value.ImplicitIt {
_ = checker.scope.Define(&Symbol{Name: "it", Kind: VariableSymbol, Type: UnknownType{}})
}
for _, param := range e.Params {
bindings[param.Name] = false
for _, param := range value.Params {
typ, _ := checker.semantic.ResolveType(param.Type)
_ = checker.scope.Define(&Symbol{Name: param.Name, Kind: VariableSymbol, Type: typ})
}
return c.checkBlock(e.Body, bindings)
return checker.checkStmts(value.Body)
}
return nil
}
func (c *mutabilityChecker) pushScope() {
c.scopes = append(c.scopes, map[string]bool{})
}
func (c *mutabilityChecker) popScope() {
if len(c.scopes) == 0 {
return
func (checker *mutabilityChecker) requireMutable(name string, position int) error {
if symbol, ok := checker.scope.Lookup(name); ok && symbol.Kind == VariableSymbol {
if symbol.Mutable {
return nil
}
return immutableAssignmentError(name, position)
}
c.scopes = c.scopes[:len(c.scopes)-1]
}
func (c *mutabilityChecker) define(name string, mutable bool) {
if len(c.scopes) == 0 {
c.pushScope()
}
c.scopes[len(c.scopes)-1][name] = mutable
}
func (c *mutabilityChecker) requireMutable(name string, pos int) error {
for i := len(c.scopes) - 1; i >= 0; i-- {
if mutable, ok := c.scopes[i][name]; ok {
if mutable {
if checker.class != nil {
if field, ok := checker.class.Fields[name]; ok {
if field.Mutable {
return nil
}
return immutableAssignmentError(name, pos)
return immutableAssignmentError(name, position)
}
}
if mutable, ok := c.classFields[name]; ok {
if mutable {
return nil
}
return immutableAssignmentError(name, pos)
}
if mutable, ok := c.workerFields[name]; ok {
if mutable {
return nil
}
return immutableAssignmentError(name, pos)
}
return nil
}
func immutableAssignmentError(name string, pos int) error {
if pos > 0 {
return fmt.Errorf("cannot reassign immutable name %s at %d", name, pos)
func immutableAssignmentError(name string, position int) error {
if position > 0 {
return fmt.Errorf("cannot reassign immutable name %s at %d", name, position)
}
return fmt.Errorf("cannot reassign immutable name %s", name)
}

View file

@ -171,9 +171,9 @@ func sqlChainResultType(expr Expr) (string, bool) {
}
switch terminal {
case "fetch":
return "Result<List<*" + resultType + ">, Error>", true
return "Result<List<" + resultType + ">, Error>", true
case "single":
return "Result<*" + resultType + ", Error>", true
return "Result<" + resultType + ", Error>", true
case "iterator":
return "Result<GotlinSQLIterator<" + resultType + ">, Error>", true
default:
@ -215,7 +215,7 @@ func (g *goGenerator) sqlClass(root CallExpr, operation string) (ClassDecl, erro
if len(root.TypeArgs) != 1 {
return ClassDecl{}, fmt.Errorf("sql.%s expects exactly one row type", operation)
}
class, ok := g.classes[root.TypeArgs[0]]
class, ok := g.semantic.Classes[root.TypeArgs[0]]
if !ok {
return ClassDecl{}, fmt.Errorf("SQL row class %q does not exist", root.TypeArgs[0])
}
@ -561,7 +561,7 @@ func (g *goGenerator) sqlProjection(call CallExpr, rowClass ClassDecl, method st
if !ok {
return ClassDecl{}, nil, fmt.Errorf("%s projection must construct a local data class", method)
}
projection, ok := g.classes[callee.Name]
projection, ok := g.semantic.Classes[callee.Name]
if !ok || !projection.Data {
return ClassDecl{}, nil, fmt.Errorf("%s projection type %s must be a data class", method, callee.Name)
}
@ -1190,10 +1190,6 @@ func stmtsMatch(stmts []Stmt, match func(Expr) bool) bool {
if exprMatches(s.Value, match) {
return true
}
case GoStmt:
if exprMatches(s.Value, match) {
return true
}
case DeferStmt:
if exprMatches(s.Value, match) {
return true
@ -1214,12 +1210,6 @@ func stmtsMatch(stmts []Stmt, match func(Expr) bool) bool {
if exprMatches(s.Source, match) || stmtsMatch(s.Body, match) {
return true
}
case SelectStmt:
for _, c := range s.Cases {
if exprMatches(c.Source, match) || stmtsMatch(c.Body, match) {
return true
}
}
case TryCatchStmt:
if stmtsMatch(s.TryBody, match) || stmtsMatch(s.CatchBody, match) {
return true
@ -1242,17 +1232,5 @@ func programExprMatches(program *Program, match func(Expr) bool) bool {
}
}
}
for _, worker := range program.Workers {
for _, field := range worker.Fields {
if exprMatches(field.Value, match) {
return true
}
}
for _, fn := range worker.Methods {
if stmtsMatch(fn.Body, match) {
return true
}
}
}
return false
}

View file

@ -34,7 +34,11 @@ func TestSQLGeneratedNullableAndLockingMetadata(t *testing.T) {
if row.Fields[3].Type != "time.Time?" {
t.Fatalf("publishedAt type = %q, want time.Time?", row.Fields[3].Type)
}
if got := mapGoType(row.Fields[3].Type); got != "*time.Time" {
typ, err := ParseType(row.Fields[3].Type)
if err != nil {
t.Fatal(err)
}
if got := renderGoType(typ); got != "*time.Time" {
t.Fatalf("mapped nullable timestamp = %q, want *time.Time", got)
}
@ -64,7 +68,7 @@ func TestSQLTypedProjectionExecution(t *testing.T) {
import context
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
fun events(pool: *pgxpool.Pool, ctx: context.Context): List<*EventProjection> {
fun events(pool: *pgxpool.Pool, ctx: context.Context): List<EventProjection> {
return sql.from<EventRow>()
.select { row -> EventProjection(row.id, row.payload) }
.orderBy { it.createdAt }
@ -94,7 +98,7 @@ func TestSQLInsertOmitsGeneratedAndReturnsProjection(t *testing.T) {
import context
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
fun create(row: EventRow, pool: *pgxpool.Pool, ctx: context.Context): *EventProjection {
fun create(row: EventRow, pool: *pgxpool.Pool, ctx: context.Context): EventProjection {
return sql.insert<EventRow>(row)
.returning { value -> EventProjection(value.id, value.payload) }
.single(pool, ctx).unwrap()
@ -126,7 +130,7 @@ func TestSQLTypedUpdateAndReturning(t *testing.T) {
import context
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
fun claim(id: String, payload: String, pool: *pgxpool.Pool, ctx: context.Context): *EventProjection {
fun claim(id: String, payload: String, pool: *pgxpool.Pool, ctx: context.Context): EventProjection {
return sql.update<EventRow>()
.set { row ->
set(row.payload, payload)
@ -154,7 +158,7 @@ func TestSQLDeleteReturningFullRow(t *testing.T) {
import context
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
fun remove(id: String, pool: *pgxpool.Pool, ctx: context.Context): *EventRow {
fun remove(id: String, pool: *pgxpool.Pool, ctx: context.Context): EventRow {
return sql.delete<EventRow>()
.where { it.id == id }
.returning { it }
@ -245,7 +249,7 @@ func TestRejectExpandedInvalidSQLQueries(t *testing.T) {
},
{
name: "write execution without returning",
src: eventSQLSource + `fun query(pool: Any, ctx: Any): *EventRow { return sql.delete<EventRow>().single(pool, ctx).unwrap() }`,
src: eventSQLSource + `fun query(pool: Any, ctx: Any): EventRow { return sql.delete<EventRow>().single(pool, ctx).unwrap() }`,
want: "requires returning()",
},
{

View file

@ -94,7 +94,7 @@ func TestGenerateSQLFetchTerminal(t *testing.T) {
import context
import pgxpool "github.com/jackc/pgx/v5/pgxpool"
fun accounts(pool: *pgxpool.Pool, ctx: context.Context, customerId: String): List<*AccountRow> {
fun accounts(pool: *pgxpool.Pool, ctx: context.Context, customerId: String): List<AccountRow> {
return sql.from<AccountRow>()
.where { it.customerId == customerId }
.fetch(pool, ctx).unwrap()
@ -304,12 +304,12 @@ fun query(row: OtherRow): GotlinSQLQuery { return sql.insert<AccountRow>(row).on
},
{
name: "fetch missing arguments",
src: accountRowSource + `fun query(): List<*AccountRow> { return sql.from<AccountRow>().fetch().unwrap() }`,
src: accountRowSource + `fun query(): List<AccountRow> { return sql.from<AccountRow>().fetch().unwrap() }`,
want: "fetch() expects exactly pool and ctx positional arguments",
},
{
name: "single extra argument",
src: accountRowSource + `fun query(pool: Any, ctx: Any): *AccountRow { return sql.from<AccountRow>().single(pool, ctx, ctx).unwrap() }`,
src: accountRowSource + `fun query(pool: Any, ctx: Any): AccountRow { return sql.from<AccountRow>().single(pool, ctx, ctx).unwrap() }`,
want: "single() expects exactly pool and ctx positional arguments",
},
{
@ -319,22 +319,22 @@ fun query(row: OtherRow): GotlinSQLQuery { return sql.insert<AccountRow>(row).on
},
{
name: "fetch type arguments",
src: accountRowSource + `fun query(pool: Any, ctx: Any): List<*AccountRow> { return sql.from<AccountRow>().fetch<String>(pool, ctx) }`,
src: accountRowSource + `fun query(pool: Any, ctx: Any): List<AccountRow> { return sql.from<AccountRow>().fetch<String>(pool, ctx) }`,
want: "fetch() expects exactly pool and ctx positional arguments",
},
{
name: "fetch invalid pool type",
src: accountRowSource + `fun query(pool: String, ctx: Any): List<*AccountRow> { return sql.from<AccountRow>().fetch(pool, ctx).unwrap() }`,
src: accountRowSource + `fun query(pool: String, ctx: Any): List<AccountRow> { return sql.from<AccountRow>().fetch(pool, ctx).unwrap() }`,
want: "pool argument has non-query type String",
},
{
name: "single invalid context type",
src: accountRowSource + `fun query(pool: Any, ctx: Int): *AccountRow { return sql.from<AccountRow>().single(pool, ctx).unwrap() }`,
src: accountRowSource + `fun query(pool: Any, ctx: Int): AccountRow { return sql.from<AccountRow>().single(pool, ctx).unwrap() }`,
want: "ctx argument has non-context type Int",
},
{
name: "insert execution terminal",
src: accountRowSource + `fun query(row: AccountRow, pool: Any, ctx: Any): List<*AccountRow> { return sql.insert<AccountRow>(row).fetch(pool, ctx).unwrap() }`,
src: accountRowSource + `fun query(row: AccountRow, pool: Any, ctx: Any): List<AccountRow> { return sql.insert<AccountRow>(row).fetch(pool, ctx).unwrap() }`,
want: "fetch() is only supported for sql.from",
},
}

215
internal/lang/symbols.go Normal file
View file

@ -0,0 +1,215 @@
package lang
import (
"fmt"
gotypes "go/types"
)
type SymbolKind int
const (
VariableSymbol SymbolKind = iota
FunctionSymbolKind
ClassSymbolKind
EnumSymbolKind
ImportSymbolKind
)
type Symbol struct {
Name string
Kind SymbolKind
Type Type
Mutable bool
Decl any
}
type Scope struct {
Parent *Scope
Symbols map[string]*Symbol
}
func NewScope(parent *Scope) *Scope { return &Scope{Parent: parent, Symbols: map[string]*Symbol{}} }
func (scope *Scope) Define(symbol *Symbol) error {
if _, exists := scope.Symbols[symbol.Name]; exists {
return fmt.Errorf("duplicate symbol %s", symbol.Name)
}
scope.Symbols[symbol.Name] = symbol
return nil
}
func (scope *Scope) Lookup(name string) (*Symbol, bool) {
for current := scope; current != nil; current = current.Parent {
if symbol, ok := current.Symbols[name]; ok {
return symbol, true
}
}
return nil, false
}
type ClassSymbol struct {
Name string
Decl *ClassDecl
Fields map[string]*Symbol
Methods map[string]*Symbol
}
type SemanticProgram struct {
Syntax *Program
Global *Scope
Classes map[string]ClassDecl
ClassInfo map[string]*ClassSymbol
Functions map[string]FunctionDecl
Enums map[string]EnumDecl
Imports map[string]bool
GoPackages map[string]*gotypes.Package
HIR *HIRProgram
Mappings *mappingState
}
func Analyze(program *Program) (*SemanticProgram, error) {
semantic := &SemanticProgram{
Syntax: program,
Global: NewScope(nil),
Classes: map[string]ClassDecl{},
ClassInfo: map[string]*ClassSymbol{},
Functions: map[string]FunctionDecl{},
Enums: map[string]EnumDecl{},
Imports: map[string]bool{},
GoPackages: map[string]*gotypes.Package{},
Mappings: &mappingState{functions: map[string]string{}},
}
for index := range program.Classes {
decl := &program.Classes[index]
class := &ClassSymbol{Name: decl.Name, Decl: decl, Fields: map[string]*Symbol{}, Methods: map[string]*Symbol{}}
semantic.Classes[decl.Name] = *decl
semantic.ClassInfo[decl.Name] = class
if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: ClassSymbolKind, Type: ClassType{Class: class}, Decl: decl}); err != nil {
return nil, err
}
}
for index := range program.Enums {
decl := &program.Enums[index]
semantic.Enums[decl.Name] = *decl
if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: EnumSymbolKind, Type: NamedType{Name: decl.Name}, Decl: decl}); err != nil {
return nil, err
}
}
for _, imported := range program.Imports {
name := imported.Alias
if name == "" {
name = defaultImportAlias(imported)
}
semantic.Imports[name] = true
if importedPackage, err := importGoPackage(imported); err == nil {
semantic.GoPackages[name] = importedPackage
}
if existing, ok := semantic.Global.Lookup(name); ok && existing.Kind == ImportSymbolKind {
continue
}
if err := semantic.Global.Define(&Symbol{Name: name, Kind: ImportSymbolKind, Type: NamedType{Name: name}, Decl: imported}); err != nil {
return nil, err
}
}
for index := range program.Functions {
decl := &program.Functions[index]
semantic.Functions[decl.Name] = *decl
typ, err := semantic.functionType(decl.Params, decl.ReturnType)
if err != nil {
return nil, fmt.Errorf("function %s: %w", decl.Name, err)
}
if err := semantic.Global.Define(&Symbol{Name: decl.Name, Kind: FunctionSymbolKind, Type: typ, Decl: decl}); err != nil {
return nil, err
}
}
for index := range program.Classes {
decl := &program.Classes[index]
class := semantic.ClassInfo[decl.Name]
for fieldIndex := range decl.Fields {
field := &decl.Fields[fieldIndex]
typ, err := semantic.ResolveType(field.Type)
if err != nil {
return nil, fmt.Errorf("field %s.%s: %w", decl.Name, field.Name, err)
}
class.Fields[field.Name] = &Symbol{Name: field.Name, Kind: VariableSymbol, Type: typ, Mutable: field.Mutable, Decl: field}
}
for methodIndex := range decl.Methods {
method := &decl.Methods[methodIndex]
typ, err := semantic.functionType(method.Params, method.ReturnType)
if err != nil {
return nil, fmt.Errorf("method %s.%s: %w", decl.Name, method.Name, err)
}
class.Methods[method.Name] = &Symbol{Name: method.Name, Kind: FunctionSymbolKind, Type: typ, Decl: method}
}
}
if err := validateMutability(semantic); err != nil {
return nil, err
}
resolver := semanticResolver{program: semantic}
if err := resolver.resolve(); err != nil {
return nil, err
}
return semantic, nil
}
func (semantic *SemanticProgram) ResolveType(text string) (Type, error) {
typ, err := ParseType(text)
if err != nil {
return nil, err
}
resolved := resolveClassTypes(typ, semantic.ClassInfo)
if err := validateNoClassPointer(resolved); err != nil {
return nil, err
}
return resolved, nil
}
func validateNoClassPointer(typ Type) error {
switch value := typ.(type) {
case GoPointerType:
if class, ok := value.Element.(ClassType); ok {
return fmt.Errorf("Gotlin class %s is already reference-valued; remove '*'", class.Class.Name)
}
return validateNoClassPointer(value.Element)
case NullableType:
return validateNoClassPointer(value.Element)
case GenericType:
for _, arg := range value.Args {
if err := validateNoClassPointer(arg); err != nil {
return err
}
}
case FunctionType:
for _, param := range value.Params {
if err := validateNoClassPointer(param); err != nil {
return err
}
}
return validateNoClassPointer(value.Result)
}
return nil
}
func (semantic *SemanticProgram) functionType(params []Param, result string) (Type, error) {
paramTypes := make([]Type, len(params))
for index, param := range params {
typ, err := semantic.ResolveType(param.Type)
if err != nil {
return nil, err
}
paramTypes[index] = typ
}
resultType, err := semantic.ResolveType(result)
if err != nil {
return nil, err
}
return FunctionType{Params: paramTypes, Result: resultType}, nil
}
func (semantic *SemanticProgram) GoType(text string) string {
typ, err := semantic.ResolveType(text)
if err != nil {
return ""
}
return renderGoType(typ)
}

View file

@ -15,7 +15,6 @@ const (
tokenPackage tokenKind = "PACKAGE"
tokenClass tokenKind = "CLASS"
tokenData tokenKind = "DATA"
tokenWorker tokenKind = "WORKER"
tokenInterface tokenKind = "INTERFACE"
tokenEnum tokenKind = "ENUM"
tokenMatch tokenKind = "MATCH"
@ -30,9 +29,7 @@ const (
tokenWhile tokenKind = "WHILE"
tokenFor tokenKind = "FOR"
tokenIn tokenKind = "IN"
tokenSelect tokenKind = "SELECT"
tokenReturn tokenKind = "RETURN"
tokenGo tokenKind = "GO"
tokenDefer tokenKind = "DEFER"
tokenTry tokenKind = "TRY"
tokenCatch tokenKind = "CATCH"
@ -90,7 +87,6 @@ var keywords = map[string]tokenKind{
"while": tokenWhile,
"for": tokenFor,
"in": tokenIn,
"select": tokenSelect,
"return": tokenReturn,
"defer": tokenDefer,
"try": tokenTry,

View file

@ -0,0 +1,191 @@
package lang
type TypeEnvironment struct {
Scope *Scope
Class *ClassSymbol
}
func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) Type {
resolve := func(text string) Type {
typ, err := semantic.ResolveType(text)
if err != nil {
return UnknownType{}
}
return typ
}
switch value := expr.(type) {
case IdentExpr:
if environment.Scope != nil {
if symbol, ok := environment.Scope.Lookup(value.Name); ok {
return symbol.Type
}
}
if environment.Class != nil {
if value.Name == "this" {
return ClassType{Class: environment.Class}
}
if field, ok := environment.Class.Fields[value.Name]; ok {
return field.Type
}
}
if symbol, ok := semantic.Global.Lookup(value.Name); ok {
return symbol.Type
}
case IntExpr:
return NamedType{Name: "Int"}
case FloatExpr:
return NamedType{Name: "Double"}
case StringExpr:
return NamedType{Name: "String"}
case BoolExpr:
return NamedType{Name: "Boolean"}
case NullExpr:
return NullableType{Element: UnknownType{}}
case CallExpr:
if selector, ok := value.Callee.(SelectorExpr); ok {
if receiver, ok := selector.Receiver.(IdentExpr); ok {
if semantic.Imports[receiver.Name] {
if function, ok := semantic.goSelectorType(receiver.Name, selector.Name).(FunctionType); ok {
return function.Result
}
}
if _, ok := semantic.Enums[receiver.Name]; ok {
return NamedType{Name: receiver.Name}
}
if receiver.Name == "json" && selector.Name == "decode" && len(value.TypeArgs) == 1 {
decoded := resolve(value.TypeArgs[0])
return GenericType{Base: NamedType{Name: "Result"}, Args: []Type{decoded, NamedType{Name: "Error"}}}
}
}
if selector.Name == "unwrap" || selector.Name == "unwrapOr" {
if result, ok := semantic.TypeOf(selector.Receiver, environment).(GenericType); ok && result.Base.String() == "Result" && len(result.Args) == 2 {
return result.Args[0]
}
}
if selector.Name == "mapTo" && len(value.TypeArgs) == 1 {
return resolve(value.TypeArgs[0])
}
receiverType := semantic.TypeOf(selector.Receiver, environment)
if iterator, ok := receiverType.(GenericType); ok && iterator.Base.String() == "GotlinSQLIterator" && len(iterator.Args) == 1 {
switch selector.Name {
case "next":
return NamedType{Name: "Boolean"}
case "value":
return iterator.Args[0]
case "err":
return NamedType{Name: "Error"}
case "close":
return NamedType{Name: "Unit"}
}
}
if class := classTypeOf(receiverType); class != nil {
if method, ok := class.Methods[selector.Name]; ok {
if function, ok := method.Type.(FunctionType); ok {
return function.Result
}
}
}
if function, ok := semantic.goMethodType(receiverType, selector.Name).(FunctionType); ok {
return function.Result
}
}
if ident, ok := value.Callee.(IdentExpr); ok {
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 {
return GenericType{Base: NamedType{Name: "List"}, Args: []Type{mapping.Args[0]}}
}
}
if function, ok := semantic.Functions[ident.Name]; ok {
return resolve(function.ReturnType)
}
if class, ok := semantic.ClassInfo[ident.Name]; ok {
return ClassType{Class: class}
}
}
if sqlType, ok := sqlChainResultType(value); ok {
return resolve(sqlType)
}
if len(value.NamedArgs) > 0 {
if selector, ok := value.Callee.(SelectorExpr); ok {
if receiver, ok := selector.Receiver.(IdentExpr); ok {
return NamedType{Name: receiver.Name + "." + selector.Name}
}
}
}
case SelectorExpr:
if receiver, ok := value.Receiver.(IdentExpr); ok {
if semantic.Imports[receiver.Name] {
return semantic.goSelectorType(receiver.Name, value.Name)
}
if _, ok := semantic.Enums[receiver.Name]; ok {
return NamedType{Name: receiver.Name}
}
}
if class := classTypeOf(semantic.TypeOf(value.Receiver, environment)); class != nil {
if field, ok := class.Fields[value.Name]; ok {
return field.Type
}
}
if field := semantic.goFieldType(semantic.TypeOf(value.Receiver, environment), value.Name); !isUnknownType(field) {
return field
}
case IndexExpr:
if generic, ok := semantic.TypeOf(value.Receiver, environment).(GenericType); ok && len(generic.Args) > 0 {
return generic.Args[len(generic.Args)-1]
}
case MatchExpr:
for _, matchCase := range value.Cases {
if typ := semantic.TypeOf(matchCase.Value, environment); !isUnknownType(typ) {
return typ
}
}
case EnumVariantExpr:
return resolve(value.EnumName)
case NonNullExpr:
if nullable, ok := semantic.TypeOf(value.Value, environment).(NullableType); ok {
return nullable.Element
}
case TryExpr:
if result, ok := semantic.TypeOf(value.Value, environment).(GenericType); ok && result.Base.String() == "Result" && len(result.Args) == 2 {
return result.Args[0]
}
case SafeSelectorExpr:
receiver := semantic.TypeOf(value.Receiver, environment)
if nullable, ok := receiver.(NullableType); ok {
receiver = nullable.Element
}
if class := classTypeOf(receiver); class != nil {
if field, ok := class.Fields[value.Name]; ok {
return nullableSemanticType(field.Type)
}
if method, ok := class.Methods[value.Name]; ok {
if function, ok := method.Type.(FunctionType); ok {
return nullableSemanticType(function.Result)
}
}
}
}
return UnknownType{}
}
func classTypeOf(typ Type) *ClassSymbol {
switch value := typ.(type) {
case ClassType:
return value.Class
case NullableType:
return classTypeOf(value.Element)
}
return nil
}
func nullableSemanticType(typ Type) Type {
if _, ok := typ.(NullableType); ok {
return typ
}
return NullableType{Element: typ}
}
func isUnknownType(typ Type) bool {
_, ok := typ.(UnknownType)
return ok
}

231
internal/lang/types.go Normal file
View file

@ -0,0 +1,231 @@
package lang
import (
"fmt"
"strings"
)
type Type interface {
typeNode()
String() string
}
type UnknownType struct{}
func (UnknownType) typeNode() {}
func (UnknownType) String() string { return "<unknown>" }
type NamedType struct{ Name string }
func (NamedType) typeNode() {}
func (t NamedType) String() string { return t.Name }
type ClassType struct{ Class *ClassSymbol }
func (ClassType) typeNode() {}
func (t ClassType) String() string { return t.Class.Name }
type NullableType struct{ Element Type }
func (NullableType) typeNode() {}
func (t NullableType) String() string { return t.Element.String() + "?" }
type GoPointerType struct{ Element Type }
func (GoPointerType) typeNode() {}
func (t GoPointerType) String() string { return "*" + t.Element.String() }
type FunctionType struct {
Params []Type
Result Type
}
func (FunctionType) typeNode() {}
func (t FunctionType) String() string {
params := make([]string, 0, len(t.Params))
for _, param := range t.Params {
params = append(params, param.String())
}
return "(" + strings.Join(params, ", ") + ") -> " + t.Result.String()
}
type GenericType struct {
Base Type
Args []Type
}
func (GenericType) typeNode() {}
func (t GenericType) String() string {
args := make([]string, 0, len(t.Args))
for _, arg := range t.Args {
args = append(args, arg.String())
}
return t.Base.String() + "<" + strings.Join(args, ", ") + ">"
}
func ParseType(text string) (Type, error) {
text = strings.TrimSpace(text)
if text == "" {
return UnknownType{}, nil
}
if strings.HasSuffix(text, "?") {
element, err := ParseType(strings.TrimSpace(strings.TrimSuffix(text, "?")))
if err != nil {
return nil, err
}
return NullableType{Element: element}, nil
}
if strings.HasPrefix(text, "*") {
element, err := ParseType(strings.TrimSpace(strings.TrimPrefix(text, "*")))
if err != nil {
return nil, err
}
return GoPointerType{Element: element}, nil
}
if params, result, ok := parseFunctionType(text); ok {
resolvedParams := make([]Type, 0, len(params))
for _, param := range params {
resolved, err := ParseType(param)
if err != nil {
return nil, err
}
resolvedParams = append(resolvedParams, resolved)
}
resolvedResult, err := ParseType(result)
if err != nil {
return nil, err
}
return FunctionType{Params: resolvedParams, Result: resolvedResult}, nil
}
if base, args, ok := parseGenericType(text); ok {
resolvedBase, err := ParseType(base)
if err != nil {
return nil, err
}
resolvedArgs := make([]Type, 0, len(args))
for _, arg := range args {
resolved, err := ParseType(arg)
if err != nil {
return nil, err
}
resolvedArgs = append(resolvedArgs, resolved)
}
return GenericType{Base: resolvedBase, Args: resolvedArgs}, nil
}
if strings.ContainsAny(text, "<>?()") {
return nil, fmt.Errorf("invalid type %q", text)
}
return NamedType{Name: text}, nil
}
func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type {
switch value := typ.(type) {
case NamedType:
if class, ok := classes[value.Name]; ok {
return ClassType{Class: class}
}
return value
case NullableType:
return NullableType{Element: resolveClassTypes(value.Element, classes)}
case GoPointerType:
return GoPointerType{Element: resolveClassTypes(value.Element, classes)}
case FunctionType:
params := make([]Type, len(value.Params))
for i, param := range value.Params {
params[i] = resolveClassTypes(param, classes)
}
return FunctionType{Params: params, Result: resolveClassTypes(value.Result, classes)}
case GenericType:
args := make([]Type, len(value.Args))
for i, arg := range value.Args {
args[i] = resolveClassTypes(arg, classes)
}
return GenericType{Base: resolveClassTypes(value.Base, classes), Args: args}
default:
return typ
}
}
func typeEqual(left, right Type) bool { return left.String() == right.String() }
func renderGoType(typ Type) string {
switch value := typ.(type) {
case UnknownType:
return ""
case ClassType:
return "*" + value.Class.Name
case NullableType:
element := renderGoType(value.Element)
switch value.Element.(type) {
case ClassType, GoPointerType:
return element
}
if element == "error" || element == "any" {
return element
}
return "*" + element
case GoPointerType:
return "*" + renderGoType(value.Element)
case FunctionType:
params := make([]string, len(value.Params))
for i, param := range value.Params {
params[i] = renderGoType(param)
}
result := renderGoType(value.Result)
if result == "" {
return "func(" + strings.Join(params, ", ") + ")"
}
return "func(" + strings.Join(params, ", ") + ") " + result
case GenericType:
base := value.Base.String()
args := make([]string, len(value.Args))
for i, arg := range value.Args {
args[i] = renderGoType(arg)
}
switch base {
case "List", "MutableList":
return "[]" + args[0]
case "Map", "MutableMap":
return "map[" + args[0] + "]" + args[1]
case "Channel":
return "chan " + args[0]
case "Result":
if args[0] == "" {
args[0] = "struct{}"
}
return "GotlinResult[" + args[0] + "]"
case "GotlinSQLIterator":
argument := args[0]
if class, ok := value.Args[0].(ClassType); ok {
argument = class.Class.Name
}
return "*GotlinSQLIterator[" + argument + "]"
}
return base + "[" + strings.Join(args, ", ") + "]"
case NamedType:
switch value.Name {
case "Int":
return "int"
case "Long":
return "int64"
case "Float", "Double":
return "float64"
case "String":
return "string"
case "Any":
return "any"
case "ByteSlice":
return "[]byte"
case "Boolean":
return "bool"
case "Unit":
return ""
case "Error":
return "error"
default:
return value.Name
}
default:
return ""
}
}

View file

@ -0,0 +1,85 @@
package lang
import "testing"
func TestStructuralTypeParsing(t *testing.T) {
typ, err := ParseType("(String?, List<User>) -> Result<User, Error>")
if err != nil {
t.Fatal(err)
}
function, ok := typ.(FunctionType)
if !ok || len(function.Params) != 2 {
t.Fatalf("unexpected function type: %#v", typ)
}
if _, ok := function.Params[0].(NullableType); !ok {
t.Fatalf("expected nullable parameter: %#v", function.Params[0])
}
result, ok := function.Result.(GenericType)
if !ok || result.Base.String() != "Result" || len(result.Args) != 2 {
t.Fatalf("unexpected result type: %#v", function.Result)
}
}
func TestClassReferenceSemanticsStayOutOfSyntaxAST(t *testing.T) {
program, err := Parse(`package demo
class Repository
class Service(val repository: Repository)
fun create(): Service { return Service(Repository()) }`)
if err != nil {
t.Fatal(err)
}
semantic, err := Analyze(program)
if err != nil {
t.Fatal(err)
}
if program.Classes[1].Fields[0].Type != "Repository" || program.Functions[0].ReturnType != "Service" {
t.Fatalf("syntax types were rewritten: %#v %#v", program.Classes[1].Fields[0], program.Functions[0])
}
if _, err := GenerateGo(program); err != nil {
t.Fatal(err)
}
if program.Classes[1].Fields[0].Type != "Repository" || program.Functions[0].ReturnType != "Service" {
t.Fatal("Go generation mutated syntax type spelling")
}
fieldType := semantic.ClassInfo["Service"].Fields["repository"].Type
if _, ok := fieldType.(ClassType); !ok {
t.Fatalf("expected semantic class type, got %#v", fieldType)
}
if got := semantic.GoType("List<Repository>"); got != "[]*Repository" {
t.Fatalf("unexpected Go type: %s", got)
}
}
func TestResolverBuildsTypedHIR(t *testing.T) {
program, err := Parse(`package demo
import strconv
fun parse(value: String): Result<Int, Error> {
val parsed = strconv.atoi(value)?
return Result.Ok(parsed)
}`)
if err != nil {
t.Fatal(err)
}
semantic, err := Analyze(program)
if err != nil {
t.Fatal(err)
}
if semantic.HIR == nil || len(semantic.HIR.Functions) != 1 {
t.Fatal("typed HIR function was not created")
}
decl := program.Functions[0].Body[0].(VarDecl)
attempt := decl.Value.(TryExpr)
if attempt.Meta.Semantic == nil || attempt.Meta.Semantic.Meaning != PropagateResultExpr || attempt.Meta.Semantic.Type.String() != "Int" {
t.Fatalf("unexpected propagation HIR: %#v", attempt.Meta.Semantic)
}
if _, ok := attempt.Meta.Semantic.Node.(HIRPropagateResult); !ok {
t.Fatalf("propagation did not lower to HIRPropagateResult: %#v", attempt.Meta.Semantic.Node)
}
call := attempt.Value.(CallExpr)
if call.Meta.Semantic == nil || call.Meta.Semantic.Meaning != GoCallExpr || call.Meta.Semantic.Type.String() != "Result<Int, Error>" {
t.Fatalf("external call was not resolved: %#v", call.Meta.Semantic)
}
if _, ok := call.Meta.Semantic.Node.(HIRGoCall); !ok {
t.Fatalf("external call did not lower to HIRGoCall: %#v", call.Meta.Semantic.Node)
}
}