Introduce typed semantic analysis pipeline
This commit is contained in:
parent
117d188194
commit
f4cd4f4458
30 changed files with 2079 additions and 2381 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue