Introduce typed semantic analysis pipeline

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

View file

@ -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>",

View file

@ -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)
}
}

View file

@ -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 == "" {