Introduce typed semantic analysis pipeline
This commit is contained in:
parent
117d188194
commit
f4cd4f4458
30 changed files with 2079 additions and 2381 deletions
14
README.md
14
README.md
|
|
@ -8,6 +8,11 @@ This is the practical boundary of the prototype:
|
|||
- It targets the Go toolchain by generating valid Go source and building through `go build`.
|
||||
- It is not a direct integration into Go's internal `cmd/compile` backend APIs.
|
||||
|
||||
The compiler keeps source spelling in its syntax AST, then builds lexical
|
||||
symbols, structural semantic types, resolved expression meanings, and typed
|
||||
HIR before Go emission. Class reference semantics live in `ClassType`; only
|
||||
the semantic Type-to-Go mapping turns a class such as `User` into `*User`.
|
||||
|
||||
## Supported language slice
|
||||
|
||||
- `fun` declarations
|
||||
|
|
@ -170,8 +175,8 @@ destructuring, or `.unwrap()`.
|
|||
|
||||
Gotlin-defined `class` and `data class` values are references automatically,
|
||||
including nested generic types such as `List<User>`. Explicit pointer syntax is
|
||||
still supported for compatibility. Go interop remains explicit, for example
|
||||
`*http.Request` and `*pgxpool.Pool`.
|
||||
reserved for Go interop, for example `*http.Request` and `*pgxpool.Pool`;
|
||||
applying `*` to a Gotlin class is a compile error.
|
||||
|
||||
Enums whose variants carry no payload are represented as string-backed values.
|
||||
The exact variant identifier is used for JSON and PostgreSQL text values:
|
||||
|
|
@ -251,8 +256,9 @@ go run ./cmd/gotlinc run ./examples/hello.gt
|
|||
|
||||
Gotlin coroutines use Go goroutines underneath, but expose only structured
|
||||
scopes. A scope waits for its children, propagates child failures, and cancels
|
||||
sibling coroutine contexts. The removed `worker` and bare `go` forms are not
|
||||
valid Gotlin syntax.
|
||||
sibling coroutine contexts. The removed `worker`, bare `go`, and channel
|
||||
`select` forms are not valid Gotlin syntax; use coroutine scopes, `delay`, and
|
||||
explicit channel `read()`/`send()` operations.
|
||||
|
||||
```kotlin
|
||||
suspend fun load(): Int {
|
||||
|
|
|
|||
|
|
@ -306,6 +306,9 @@ func buildDocumentState(text string) documentState {
|
|||
return state
|
||||
}
|
||||
state.program = program
|
||||
if _, err := lang.Analyze(program); err != nil {
|
||||
state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err))
|
||||
}
|
||||
state.symbols = indexSymbols(text, program)
|
||||
state.diagnostics = semanticDiagnostics(text, program, state.symbols)
|
||||
if _, err := lang.GenerateGo(program); err != nil {
|
||||
|
|
@ -681,38 +684,6 @@ func indexSymbols(text string, program *lang.Program) []symbol {
|
|||
})
|
||||
}
|
||||
}
|
||||
workerRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*worker\s+([A-Za-z_][A-Za-z0-9_]*)`))
|
||||
for i, decl := range program.Workers {
|
||||
r := rng{}
|
||||
if i < len(workerRanges) {
|
||||
r = workerRanges[i]
|
||||
}
|
||||
symbols = append(symbols, symbol{
|
||||
Name: decl.Name,
|
||||
Kind: symbolKindClass,
|
||||
Detail: renderWorkerSignature(decl),
|
||||
Range: r,
|
||||
})
|
||||
for _, field := range decl.Fields {
|
||||
symbols = append(symbols, symbol{
|
||||
Name: field.Name,
|
||||
Kind: symbolKindField,
|
||||
Detail: renderWorkerFieldSignature(decl.Name, field),
|
||||
Range: r,
|
||||
Targets: []string{decl.Name},
|
||||
})
|
||||
}
|
||||
for _, method := range decl.Methods {
|
||||
symbols = append(symbols, symbol{
|
||||
Name: method.Name,
|
||||
Kind: symbolKindMethod,
|
||||
Detail: renderMethodSignature(decl.Name, method),
|
||||
Range: r,
|
||||
Targets: []string{decl.Name},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
funcRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*fun\s+([A-Za-z_][A-Za-z0-9_]*)`))
|
||||
for i, fn := range program.Functions {
|
||||
detail := renderFunctionSignature(fn)
|
||||
|
|
@ -809,9 +780,6 @@ func semanticDiagnosticsWithPackage(text string, program *lang.Program, symbols
|
|||
for _, decl := range program.Classes {
|
||||
types[decl.Name] = true
|
||||
}
|
||||
for _, decl := range program.Workers {
|
||||
types[decl.Name] = true
|
||||
}
|
||||
for _, decl := range program.Enums {
|
||||
types[decl.Name] = true
|
||||
}
|
||||
|
|
@ -825,9 +793,6 @@ func semanticDiagnosticsWithPackage(text string, program *lang.Program, symbols
|
|||
for _, decl := range sibling.Classes {
|
||||
types[decl.Name] = true
|
||||
}
|
||||
for _, decl := range sibling.Workers {
|
||||
types[decl.Name] = true
|
||||
}
|
||||
for _, decl := range sibling.Enums {
|
||||
types[decl.Name] = true
|
||||
}
|
||||
|
|
@ -846,17 +811,6 @@ func semanticDiagnosticsWithPackage(text string, program *lang.Program, symbols
|
|||
diagnostics = append(diagnostics, functionSemanticDiagnostics(text, method, functions, imports, types, fields)...)
|
||||
}
|
||||
}
|
||||
for _, worker := range program.Workers {
|
||||
fields := map[string]bool{
|
||||
"this": true,
|
||||
}
|
||||
for _, field := range worker.Fields {
|
||||
fields[field.Name] = true
|
||||
}
|
||||
for _, method := range worker.Methods {
|
||||
diagnostics = append(diagnostics, functionSemanticDiagnostics(text, method, functions, imports, types, fields)...)
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(diagnostics, func(i, j int) bool {
|
||||
if diagnostics[i].Range.Start.Line != diagnostics[j].Range.Start.Line {
|
||||
|
|
@ -913,8 +867,6 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma
|
|||
}
|
||||
case lang.ThrowStmt:
|
||||
walkExpr(s.Value, scope)
|
||||
case lang.GoStmt:
|
||||
walkExpr(s.Value, scope)
|
||||
case lang.DeferStmt:
|
||||
walkExpr(s.Value, scope)
|
||||
case lang.ExprStmt:
|
||||
|
|
@ -934,13 +886,6 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma
|
|||
bodyScope := copyScope(scope)
|
||||
bodyScope[s.Name] = true
|
||||
walkStmts(s.Body, bodyScope)
|
||||
case lang.SelectStmt:
|
||||
for _, c := range s.Cases {
|
||||
walkExpr(c.Source, scope)
|
||||
caseScope := copyScope(scope)
|
||||
caseScope["it"] = true
|
||||
walkStmts(c.Body, caseScope)
|
||||
}
|
||||
case lang.MatchStmt:
|
||||
walkExpr(s.Value, scope)
|
||||
for _, matchCase := range s.Cases {
|
||||
|
|
@ -973,25 +918,6 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma
|
|||
walkExpr(e.Left, scope)
|
||||
walkExpr(e.Right, scope)
|
||||
case lang.CallExpr:
|
||||
if ident, ok := e.Callee.(lang.IdentExpr); ok {
|
||||
switch ident.Name {
|
||||
case "after", "every":
|
||||
if len(e.Args) != 1 {
|
||||
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects exactly one Int argument"))
|
||||
}
|
||||
if len(e.Args) == 1 {
|
||||
switch e.Args[0].(type) {
|
||||
case lang.StringExpr, lang.BoolExpr, lang.NullExpr:
|
||||
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects an Int argument"))
|
||||
}
|
||||
}
|
||||
if ident.Name == "every" && len(e.Args) == 1 {
|
||||
if value, ok := staticIntExprValue(e.Args[0]); ok && value <= 0 {
|
||||
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, "every(ms) requires ms > 0"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walkExpr(e.Callee, scope)
|
||||
for _, arg := range e.Args {
|
||||
walkExpr(arg, scope)
|
||||
|
|
@ -1339,10 +1265,6 @@ func renderClassSignature(decl lang.ClassDecl) string {
|
|||
return b.String()
|
||||
}
|
||||
|
||||
func renderWorkerSignature(decl lang.WorkerDecl) string {
|
||||
return "worker " + decl.Name
|
||||
}
|
||||
|
||||
func renderMethodSignature(className string, fn lang.FunctionDecl) string {
|
||||
return className + "." + renderFunctionSignature(fn)
|
||||
}
|
||||
|
|
@ -1355,21 +1277,6 @@ func renderFieldSignature(className string, field lang.FieldDecl) string {
|
|||
return className + "." + keyword + " " + field.Name + ": " + field.Type
|
||||
}
|
||||
|
||||
func renderWorkerFieldSignature(workerName string, field lang.WorkerFieldDecl) string {
|
||||
keyword := "val"
|
||||
if field.Mutable {
|
||||
keyword = "var"
|
||||
}
|
||||
typ := field.Type
|
||||
if typ == "" {
|
||||
typ = inferWorkerFieldType(field)
|
||||
}
|
||||
if typ != "" {
|
||||
return workerName + "." + keyword + " " + field.Name + ": " + typ
|
||||
}
|
||||
return workerName + "." + keyword + " " + field.Name
|
||||
}
|
||||
|
||||
func renderVariableSignature(name string, mutable bool, typ string) string {
|
||||
keyword := "val"
|
||||
if mutable {
|
||||
|
|
@ -1390,10 +1297,6 @@ func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
|
|||
for _, class := range program.Classes {
|
||||
classes[class.Name] = class
|
||||
}
|
||||
workers := map[string]lang.WorkerDecl{}
|
||||
for _, worker := range program.Workers {
|
||||
workers[worker.Name] = worker
|
||||
}
|
||||
|
||||
var out []variableDeclInfo
|
||||
var walkStmts func(stmts []lang.Stmt, scope map[string]string)
|
||||
|
|
@ -1409,14 +1312,9 @@ func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
|
|||
if _, ok := classes[callee.Name]; ok {
|
||||
return []string{callee.Name}
|
||||
}
|
||||
if _, ok := workers[callee.Name]; ok {
|
||||
return []string{callee.Name}
|
||||
}
|
||||
switch callee.Name {
|
||||
case "println":
|
||||
return []string{"Unit"}
|
||||
case "after", "every":
|
||||
return []string{"Channel<time.Time>"}
|
||||
case "Channel":
|
||||
if len(call.TypeArgs) == 1 {
|
||||
return []string{"Channel<" + call.TypeArgs[0] + ">"}
|
||||
|
|
@ -1562,16 +1460,6 @@ func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
|
|||
}
|
||||
}
|
||||
}
|
||||
if worker, ok := workers[trimmed]; ok {
|
||||
for _, field := range worker.Fields {
|
||||
if field.Name == e.Name {
|
||||
if field.Type != "" {
|
||||
return field.Type
|
||||
}
|
||||
return inferWorkerFieldType(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
|
|
@ -1584,7 +1472,12 @@ func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
|
|||
case lang.VarDecl:
|
||||
typ := s.Type
|
||||
if typ == "" {
|
||||
typ = inferExprType(s.Value, scope)
|
||||
resolved := lang.ResolvedType(s.Value)
|
||||
if resolved.String() != "<unknown>" {
|
||||
typ = resolved.String()
|
||||
} else {
|
||||
typ = inferExprType(s.Value, scope)
|
||||
}
|
||||
}
|
||||
out = append(out, variableDeclInfo{Name: s.Name, Mutable: s.Mutable, Type: typ})
|
||||
scope[s.Name] = typ
|
||||
|
|
@ -1613,16 +1506,6 @@ func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
|
|||
case lang.WhileStmt:
|
||||
bodyScope := copyTypeScope(scope)
|
||||
walkStmts(s.Body, bodyScope)
|
||||
case lang.SelectStmt:
|
||||
for _, c := range s.Cases {
|
||||
caseScope := copyTypeScope(scope)
|
||||
if elemType, ok := channelElementType(inferExprType(c.Source, scope)); ok {
|
||||
caseScope["it"] = elemType
|
||||
} else {
|
||||
caseScope["it"] = "any"
|
||||
}
|
||||
walkStmts(c.Body, caseScope)
|
||||
}
|
||||
case lang.TryCatchStmt:
|
||||
tryScope := copyTypeScope(scope)
|
||||
walkStmts(s.TryBody, tryScope)
|
||||
|
|
@ -1653,23 +1536,6 @@ func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
|
|||
walkStmts(method.Body, scope)
|
||||
}
|
||||
}
|
||||
for _, worker := range program.Workers {
|
||||
fieldScope := map[string]string{}
|
||||
for _, field := range worker.Fields {
|
||||
typ := field.Type
|
||||
if typ == "" {
|
||||
typ = inferWorkerFieldType(field)
|
||||
}
|
||||
fieldScope[field.Name] = typ
|
||||
}
|
||||
for _, method := range worker.Methods {
|
||||
scope := copyTypeScope(fieldScope)
|
||||
for _, param := range method.Params {
|
||||
scope[param.Name] = param.Type
|
||||
}
|
||||
walkStmts(method.Body, scope)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
@ -1693,44 +1559,6 @@ func channelElementType(typ string) (string, bool) {
|
|||
return inner, true
|
||||
}
|
||||
|
||||
func staticIntExprValue(expr lang.Expr) (int, bool) {
|
||||
switch e := expr.(type) {
|
||||
case lang.IntExpr:
|
||||
v, err := strconv.Atoi(e.Value)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return v, true
|
||||
case lang.UnaryExpr:
|
||||
if e.Op != "-" {
|
||||
return 0, false
|
||||
}
|
||||
v, ok := staticIntExprValue(e.Value)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return -v, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func inferWorkerFieldType(field lang.WorkerFieldDecl) string {
|
||||
if field.Type != "" {
|
||||
return field.Type
|
||||
}
|
||||
switch field.Value.(type) {
|
||||
case lang.IntExpr:
|
||||
return "Int"
|
||||
case lang.StringExpr:
|
||||
return "String"
|
||||
case lang.BoolExpr:
|
||||
return "Boolean"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func selectorPathLang(expr lang.Expr) (string, bool) {
|
||||
switch e := expr.(type) {
|
||||
case lang.IdentExpr:
|
||||
|
|
@ -2199,8 +2027,6 @@ var builtinDetails = map[string]string{
|
|||
"println": "fun println(value: Any): Unit",
|
||||
"runCatching": "fun runCatching(block: () -> Unit): Result",
|
||||
"Channel": "fun Channel<T>(capacity: Int = 0): Channel<T>",
|
||||
"after": "fun after(ms: Int): Channel<time.Time>",
|
||||
"every": "fun every(ms: Int): Channel<time.Time>",
|
||||
"listOf": "fun listOf<T>(values: T...): List<T>",
|
||||
"mutableListOf": "fun mutableListOf<T>(values: T...): MutableList<T>",
|
||||
"mapOf": "fun mapOf<K, V>(pairs: Any...): Map<K, V>",
|
||||
|
|
|
|||
|
|
@ -707,7 +707,7 @@ fun main() {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuildDocumentStateChannelsAndSelectSemantics(t *testing.T) {
|
||||
func TestBuildDocumentStateChannelSemantics(t *testing.T) {
|
||||
text := strings.TrimSpace(`
|
||||
package demo
|
||||
|
||||
|
|
@ -716,13 +716,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)
|
||||
}
|
||||
|
|
@ -787,138 +782,3 @@ fun main() {
|
|||
t.Fatal("expected removed worker syntax diagnostic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDocumentStateTimerBuiltinsSemantics(t *testing.T) {
|
||||
text := strings.TrimSpace(`
|
||||
package demo
|
||||
|
||||
fun main() {
|
||||
val once = after(1000)
|
||||
val repeat = every(250)
|
||||
select {
|
||||
after(1000) -> println(it)
|
||||
every(250) -> println("tick")
|
||||
}
|
||||
println(once)
|
||||
println(repeat)
|
||||
}
|
||||
`)
|
||||
|
||||
state := buildDocumentState(text)
|
||||
if state.program == nil {
|
||||
t.Fatal("expected parsed program")
|
||||
}
|
||||
if len(state.diagnostics) != 0 {
|
||||
t.Fatalf("expected no diagnostics, got %+v", state.diagnostics)
|
||||
}
|
||||
|
||||
var onceDetail string
|
||||
var repeatDetail string
|
||||
for _, sym := range state.symbols {
|
||||
if sym.Kind != symbolKindVariable {
|
||||
continue
|
||||
}
|
||||
if sym.Name == "once" {
|
||||
onceDetail = sym.Detail
|
||||
}
|
||||
if sym.Name == "repeat" {
|
||||
repeatDetail = sym.Detail
|
||||
}
|
||||
}
|
||||
if onceDetail != "val once: Channel<time.Time>" {
|
||||
t.Fatalf("unexpected once detail: %q", onceDetail)
|
||||
}
|
||||
if repeatDetail != "val repeat: Channel<time.Time>" {
|
||||
t.Fatalf("unexpected repeat detail: %q", repeatDetail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDocumentStateEveryNonPositiveDiagnostics(t *testing.T) {
|
||||
text := strings.TrimSpace(`
|
||||
package demo
|
||||
|
||||
fun main() {
|
||||
select {
|
||||
every(0) -> println("x")
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
state := buildDocumentState(text)
|
||||
if state.program == nil {
|
||||
t.Fatal("expected parsed program")
|
||||
}
|
||||
if len(state.diagnostics) == 0 {
|
||||
t.Fatal("expected diagnostics")
|
||||
}
|
||||
found := false
|
||||
for _, d := range state.diagnostics {
|
||||
if strings.Contains(d.Message, "every(ms) requires ms > 0") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected every(ms) diagnostic, got %+v", state.diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDocumentStateAfterIntArgumentDiagnostics(t *testing.T) {
|
||||
text := strings.TrimSpace(`
|
||||
package demo
|
||||
|
||||
fun main() {
|
||||
select {
|
||||
after("1s") -> println("x")
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
state := buildDocumentState(text)
|
||||
if state.program == nil {
|
||||
t.Fatal("expected parsed program")
|
||||
}
|
||||
found := false
|
||||
for _, d := range state.diagnostics {
|
||||
if strings.Contains(d.Message, "after(ms) expects an Int argument") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected after int argument diagnostic, got %+v", state.diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHoverBuiltinEverySignature(t *testing.T) {
|
||||
text := strings.TrimSpace(`
|
||||
package demo
|
||||
|
||||
fun main() {
|
||||
every(100)
|
||||
}
|
||||
`)
|
||||
uri := "file:///tmp/demo.gt"
|
||||
s := &server{
|
||||
docs: map[string]documentState{
|
||||
uri: buildDocumentState(text),
|
||||
},
|
||||
}
|
||||
|
||||
result := s.hover(uri, position{Line: 3, Character: 5})
|
||||
if result == nil {
|
||||
t.Fatal("expected hover result")
|
||||
}
|
||||
m, ok := result.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected hover type: %T", result)
|
||||
}
|
||||
contents, ok := m["contents"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected hover contents: %+v", m)
|
||||
}
|
||||
value, _ := contents["value"].(string)
|
||||
if !strings.Contains(value, "fun every(ms: Int): Channel<time.Time>") {
|
||||
t.Fatalf("unexpected hover value: %q", value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,6 @@ func compileFiles(inputPaths []string, forceMain bool) string {
|
|||
program.Interfaces = append(program.Interfaces, parsed.Interfaces...)
|
||||
program.Enums = append(program.Enums, parsed.Enums...)
|
||||
program.Classes = append(program.Classes, parsed.Classes...)
|
||||
program.Workers = append(program.Workers, parsed.Workers...)
|
||||
program.Functions = append(program.Functions, parsed.Functions...)
|
||||
program.Embeds = append(program.Embeds, parsed.Embeds...)
|
||||
if firstSource == "" {
|
||||
|
|
|
|||
|
|
@ -36,9 +36,8 @@ fun main() {
|
|||
delay(120)
|
||||
ready.send("timer fired")
|
||||
}
|
||||
select {
|
||||
ready -> println("channel says: " + it)
|
||||
}
|
||||
val message = ready.read()
|
||||
println("channel says: " + message)
|
||||
|
||||
val answer = async<Int> {
|
||||
delay(50)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
170
internal/lang/go_types.go
Normal 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
765
internal/lang/hir.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
`)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
215
internal/lang/symbols.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
191
internal/lang/type_checker.go
Normal file
191
internal/lang/type_checker.go
Normal 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
231
internal/lang/types.go
Normal 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 ""
|
||||
}
|
||||
}
|
||||
85
internal/lang/types_test.go
Normal file
85
internal/lang/types_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ assert(language.folding?.markers?.start && language.indentationRules?.increaseIn
|
|||
const grammarSource = JSON.stringify(grammar);
|
||||
const expectedTokens = [
|
||||
"data", "class", "suspend", "private", "override", "val", "var", "if", "else",
|
||||
"while", "for", "in", "select", "return", "defer", "try", "catch",
|
||||
"while", "for", "in", "return", "defer", "try", "catch",
|
||||
"throw", "enum", "match", "package", "import", "jsonNaming", "embed", "table", "column", "id",
|
||||
"generated", "Int", "Long", "String", "Boolean", "Unit", "Double", "Float", "Any",
|
||||
"ByteSlice", "Error", "Result", "List", "MutableList", "Map", "MutableMap", "Channel", "GotlinSQLQuery",
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@
|
|||
},
|
||||
{
|
||||
"name": "keyword.control.concurrency.gotlin",
|
||||
"match": "\\b(select|defer|runBlocking|coroutineScope|launch|async|await|delay|withTimeout|isActive)\\b"
|
||||
"match": "\\b(defer|runBlocking|coroutineScope|launch|async|await|delay|withTimeout|isActive)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.exception.gotlin",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue