Simplified ast package

This commit is contained in:
Eduard Urbach 2025-02-20 14:12:25 +01:00
parent 45a36a645a
commit b655950516
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
19 changed files with 151 additions and 168 deletions

View File

@ -1,7 +1,63 @@
package ast package ast
import "git.akyoto.dev/cli/q/src/expression"
// Node is an interface used for all types of AST nodes. // Node is an interface used for all types of AST nodes.
type Node any type Node any
// AST is an abstract syntax tree which is simply a list of nodes. // AST is an abstract syntax tree which is simply a list of nodes.
type AST []Node type AST []Node
// Assert is a condition that must be true, otherwise the program stops.
type Assert struct {
Condition *expression.Expression
}
// Assign is an assignment to an existing variable or memory location.
type Assign struct {
Expression *expression.Expression
}
// Call is a function call.
type Call struct {
Expression *expression.Expression
}
// Case is a case inside a switch.
type Case struct {
Condition *expression.Expression
Body AST
}
// Define is a variable definition.
type Define struct {
Expression *expression.Expression
}
// For is a loop with a defined iteration limit.
type For struct {
Head *expression.Expression
Body AST
}
// If is a conditional branch.
type If struct {
Condition *expression.Expression
Body AST
Else AST
}
// Loop is an infinite loop.
type Loop struct {
Body AST
}
// Return is a return statement.
type Return struct {
Values []*expression.Expression
}
// Switch is a conditional branch with multiple cases.
type Switch struct {
Cases []Case
}

View File

@ -1,10 +0,0 @@
package ast
import (
"git.akyoto.dev/cli/q/src/expression"
)
// Assert represents a condition that must be true, otherwise the program stops.
type Assert struct {
Condition *expression.Expression
}

View File

@ -1,10 +0,0 @@
package ast
import (
"git.akyoto.dev/cli/q/src/expression"
)
// Assign represents an assignment to an existing variable or memory location.
type Assign struct {
Expression *expression.Expression
}

View File

@ -1,8 +0,0 @@
package ast
import "git.akyoto.dev/cli/q/src/expression"
// Call represents a function call.
type Call struct {
Expression *expression.Expression
}

View File

@ -25,6 +25,10 @@ func Count(body AST, buffer []byte, kind token.Kind, name string) uint8 {
count += Count(node.Body, buffer, kind, name) count += Count(node.Body, buffer, kind, name)
count += Count(node.Else, buffer, kind, name) count += Count(node.Else, buffer, kind, name)
case *For:
count += node.Head.Count(buffer, kind, name)
count += Count(node.Body, buffer, kind, name)
case *Loop: case *Loop:
count += Count(node.Body, buffer, kind, name) count += Count(node.Body, buffer, kind, name)

View File

@ -1,10 +0,0 @@
package ast
import (
"git.akyoto.dev/cli/q/src/expression"
)
// Define represents a variable definition.
type Define struct {
Expression *expression.Expression
}

View File

@ -1,9 +0,0 @@
package ast
import "git.akyoto.dev/cli/q/src/expression"
// For is a loop with a defined iteration limit.
type For struct {
Head *expression.Expression
Body AST
}

View File

@ -1,12 +0,0 @@
package ast
import (
"git.akyoto.dev/cli/q/src/expression"
)
// If represents an if statement.
type If struct {
Condition *expression.Expression
Body AST
Else AST
}

View File

@ -1,6 +0,0 @@
package ast
// Loop is a block of infinitely repeating instructions.
type Loop struct {
Body AST
}

View File

@ -1,18 +1,18 @@
package ast package ast
import ( import (
"git.akyoto.dev/cli/q/src/expression" "git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token" "git.akyoto.dev/cli/q/src/token"
) )
// Parse generates an AST from a list of tokens. // Parse generates an AST from a list of tokens.
func Parse(tokens []token.Token, source []byte) (AST, error) { func Parse(tokens []token.Token, file *fs.File) (AST, error) {
nodes := make(AST, 0, len(tokens)/64) nodes := make(AST, 0, len(tokens)/64)
err := EachInstruction(tokens, func(instruction token.List) error { err := eachInstruction(tokens, func(instruction token.List) error {
node, err := parseNode(instruction, source, nodes) node, err := parseInstruction(instruction, file, nodes)
if err == nil && node != nil { if node != nil {
nodes = append(nodes, node) nodes = append(nodes, node)
} }
@ -21,18 +21,3 @@ func Parse(tokens []token.Token, source []byte) (AST, error) {
return nodes, err return nodes, err
} }
// IsAssignment returns true if the expression is an assignment.
func IsAssignment(expr *expression.Expression) bool {
return expr.Token.IsAssignment()
}
// IsFunctionCall returns true if the expression is a function call.
func IsFunctionCall(expr *expression.Expression) bool {
return expr.Token.Kind == token.Call
}
// IsVariableDefinition returns true if the expression is a variable definition.
func IsVariableDefinition(expr *expression.Expression) bool {
return expr.Token.Kind == token.Define
}

View File

@ -1,10 +0,0 @@
package ast
import (
"git.akyoto.dev/cli/q/src/expression"
)
// Return represents a return statement.
type Return struct {
Values []*expression.Expression
}

View File

@ -1,16 +0,0 @@
package ast
import (
"git.akyoto.dev/cli/q/src/expression"
)
// Switch represents a switch statement.
type Switch struct {
Cases []Case
}
// Case represents a case inside a switch.
type Case struct {
Condition *expression.Expression
Body AST
}

26
src/ast/block.go Normal file
View File

@ -0,0 +1,26 @@
package ast
import (
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token"
)
// block retrieves the start and end position of a block.
func block(tokens token.List, file *fs.File) (blockStart int, blockEnd int, body AST, err error) {
blockStart = tokens.IndexKind(token.BlockStart)
blockEnd = tokens.LastIndexKind(token.BlockEnd)
if blockStart == -1 {
err = errors.New(errors.MissingBlockStart, file, tokens[0].End())
return
}
if blockEnd == -1 {
err = errors.New(errors.MissingBlockEnd, file, tokens[len(tokens)-1].End())
return
}
body, err = Parse(tokens[blockStart+1:blockEnd], file)
return
}

View File

@ -2,13 +2,13 @@ package ast
import "git.akyoto.dev/cli/q/src/token" import "git.akyoto.dev/cli/q/src/token"
// EachInstruction calls the function on each instruction. // eachInstruction calls the function on each AST node.
func EachInstruction(body token.List, call func(token.List) error) error { func eachInstruction(tokens token.List, call func(token.List) error) error {
start := 0 start := 0
groupLevel := 0 groupLevel := 0
blockLevel := 0 blockLevel := 0
for i, t := range body { for i, t := range tokens {
if start == i && t.Kind == token.NewLine { if start == i && t.Kind == token.NewLine {
start = i + 1 start = i + 1
continue continue
@ -20,7 +20,7 @@ func EachInstruction(body token.List, call func(token.List) error) error {
continue continue
} }
err := call(body[start:i]) err := call(tokens[start:i])
if err != nil { if err != nil {
return err return err
@ -44,7 +44,7 @@ func EachInstruction(body token.List, call func(token.List) error) error {
continue continue
} }
err := call(body[start : i+1]) err := call(tokens[start : i+1])
if err != nil { if err != nil {
return err return err
@ -54,8 +54,8 @@ func EachInstruction(body token.List, call func(token.List) error) error {
} }
} }
if start != len(body) { if start != len(tokens) {
return call(body[start:]) return call(tokens[start:])
} }
return nil return nil

21
src/ast/helpers.go Normal file
View File

@ -0,0 +1,21 @@
package ast
import (
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
)
// IsAssignment returns true if the expression is an assignment.
func IsAssignment(expr *expression.Expression) bool {
return expr.Token.IsAssignment()
}
// IsFunctionCall returns true if the expression is a function call.
func IsFunctionCall(expr *expression.Expression) bool {
return expr.Token.Kind == token.Call
}
// IsVariableDefinition returns true if the expression is a variable definition.
func IsVariableDefinition(expr *expression.Expression) bool {
return expr.Token.Kind == token.Define
}

View File

@ -2,15 +2,16 @@ package ast
import ( import (
"git.akyoto.dev/cli/q/src/expression" "git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token" "git.akyoto.dev/cli/q/src/token"
) )
// parseSwitch generates the cases inside a switch statement. // parseCases generates the cases inside a switch statement.
func parseSwitch(tokens token.List, source []byte) ([]Case, error) { func parseCases(tokens token.List, file *fs.File) ([]Case, error) {
var cases []Case var cases []Case
err := EachInstruction(tokens, func(caseTokens token.List) error { err := eachInstruction(tokens, func(caseTokens token.List) error {
blockStart, _, body, err := block(caseTokens, source) blockStart, _, body, err := block(caseTokens, file)
if err != nil { if err != nil {
return err return err
@ -19,7 +20,7 @@ func parseSwitch(tokens token.List, source []byte) ([]Case, error) {
conditionTokens := caseTokens[:blockStart] conditionTokens := caseTokens[:blockStart]
var condition *expression.Expression var condition *expression.Expression
if len(conditionTokens) == 1 && conditionTokens[0].Kind == token.Identifier && conditionTokens[0].Text(source) == "_" { if len(conditionTokens) == 1 && conditionTokens[0].Kind == token.Identifier && conditionTokens[0].Text(file.Bytes) == "_" {
condition = nil condition = nil
} else { } else {
condition = expression.Parse(conditionTokens) condition = expression.Parse(conditionTokens)

View File

@ -3,13 +3,14 @@ package ast
import ( import (
"git.akyoto.dev/cli/q/src/errors" "git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression" "git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token" "git.akyoto.dev/cli/q/src/token"
) )
// parseNode generates an AST node from an instruction. // parseInstruction generates an AST node from an instruction.
func parseNode(tokens token.List, source []byte, nodes AST) (Node, error) { func parseInstruction(tokens token.List, file *fs.File, nodes AST) (Node, error) {
if tokens[0].IsKeyword() { if tokens[0].IsKeyword() {
return parseKeyword(tokens, source, nodes) return parseKeyword(tokens, file, nodes)
} }
expr := expression.Parse(tokens) expr := expression.Parse(tokens)
@ -19,24 +20,24 @@ func parseNode(tokens token.List, source []byte, nodes AST) (Node, error) {
} }
switch { switch {
case IsFunctionCall(expr):
return &Call{Expression: expr}, nil
case IsVariableDefinition(expr): case IsVariableDefinition(expr):
if len(expr.Children) < 2 { if len(expr.Children) < 2 {
return nil, errors.New(errors.MissingOperand, nil, expr.Token.End()) return nil, errors.New(errors.MissingOperand, file, expr.Token.End())
} }
return &Define{Expression: expr}, nil return &Define{Expression: expr}, nil
case IsAssignment(expr): case IsAssignment(expr):
if len(expr.Children) < 2 { if len(expr.Children) < 2 {
return nil, errors.New(errors.MissingOperand, nil, expr.Token.End()) return nil, errors.New(errors.MissingOperand, file, expr.Token.End())
} }
return &Assign{Expression: expr}, nil return &Assign{Expression: expr}, nil
case IsFunctionCall(expr):
return &Call{Expression: expr}, nil
default: default:
return nil, errors.New(&errors.InvalidInstruction{Instruction: tokens.Text(source)}, nil, tokens[0].Position) return nil, errors.New(&errors.InvalidInstruction{Instruction: tokens.Text(file.Bytes)}, file, tokens[0].Position)
} }
} }

View File

@ -3,44 +3,45 @@ package ast
import ( import (
"git.akyoto.dev/cli/q/src/errors" "git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression" "git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token" "git.akyoto.dev/cli/q/src/token"
) )
// parseKeyword generates a keyword node from an instruction. // parseKeyword generates a keyword node from an instruction.
func parseKeyword(tokens token.List, source []byte, nodes AST) (Node, error) { func parseKeyword(tokens token.List, file *fs.File, nodes AST) (Node, error) {
switch tokens[0].Kind { switch tokens[0].Kind {
case token.Assert: case token.Assert:
if len(tokens) == 1 { if len(tokens) == 1 {
return nil, errors.New(errors.MissingExpression, nil, tokens[0].End()) return nil, errors.New(errors.MissingExpression, file, tokens[0].End())
} }
condition := expression.Parse(tokens[1:]) condition := expression.Parse(tokens[1:])
return &Assert{Condition: condition}, nil return &Assert{Condition: condition}, nil
case token.If: case token.If:
blockStart, _, body, err := block(tokens, source) blockStart, _, body, err := block(tokens, file)
condition := expression.Parse(tokens[1:blockStart]) condition := expression.Parse(tokens[1:blockStart])
return &If{Condition: condition, Body: body}, err return &If{Condition: condition, Body: body}, err
case token.Else: case token.Else:
_, _, body, err := block(tokens, source) _, _, body, err := block(tokens, file)
if len(nodes) == 0 { if len(nodes) == 0 {
return nil, errors.New(errors.ExpectedIfBeforeElse, nil, tokens[0].Position) return nil, errors.New(errors.ExpectedIfBeforeElse, file, tokens[0].Position)
} }
last := nodes[len(nodes)-1] last := nodes[len(nodes)-1]
ifNode, exists := last.(*If) ifNode, exists := last.(*If)
if !exists { if !exists {
return nil, errors.New(errors.ExpectedIfBeforeElse, nil, tokens[0].Position) return nil, errors.New(errors.ExpectedIfBeforeElse, file, tokens[0].Position)
} }
ifNode.Else = body ifNode.Else = body
return nil, err return nil, err
case token.For: case token.For:
blockStart, _, body, err := block(tokens, source) blockStart, _, body, err := block(tokens, file)
head := tokens[1:blockStart] head := tokens[1:blockStart]
loop := &For{ loop := &For{
@ -51,7 +52,7 @@ func parseKeyword(tokens token.List, source []byte, nodes AST) (Node, error) {
return loop, err return loop, err
case token.Loop: case token.Loop:
_, _, body, err := block(tokens, source) _, _, body, err := block(tokens, file)
return &Loop{Body: body}, err return &Loop{Body: body}, err
case token.Return: case token.Return:
@ -67,42 +68,23 @@ func parseKeyword(tokens token.List, source []byte, nodes AST) (Node, error) {
blockEnd := tokens.LastIndexKind(token.BlockEnd) blockEnd := tokens.LastIndexKind(token.BlockEnd)
if blockStart == -1 { if blockStart == -1 {
return nil, errors.New(errors.MissingBlockStart, nil, tokens[0].End()) return nil, errors.New(errors.MissingBlockStart, file, tokens[0].End())
} }
if blockEnd == -1 { if blockEnd == -1 {
return nil, errors.New(errors.MissingBlockEnd, nil, tokens[len(tokens)-1].End()) return nil, errors.New(errors.MissingBlockEnd, file, tokens[len(tokens)-1].End())
} }
body := tokens[blockStart+1 : blockEnd] body := tokens[blockStart+1 : blockEnd]
if len(body) == 0 { if len(body) == 0 {
return nil, errors.New(errors.EmptySwitch, nil, tokens[0].Position) return nil, errors.New(errors.EmptySwitch, file, tokens[0].Position)
} }
cases, err := parseSwitch(body, source) cases, err := parseCases(body, file)
return &Switch{Cases: cases}, err return &Switch{Cases: cases}, err
default: default:
return nil, errors.New(&errors.KeywordNotImplemented{Keyword: tokens[0].Text(source)}, nil, tokens[0].Position) return nil, errors.New(&errors.KeywordNotImplemented{Keyword: tokens[0].Text(file.Bytes)}, file, tokens[0].Position)
} }
} }
// block retrieves the start and end position of a block.
func block(tokens token.List, source []byte) (blockStart int, blockEnd int, body AST, err error) {
blockStart = tokens.IndexKind(token.BlockStart)
blockEnd = tokens.LastIndexKind(token.BlockEnd)
if blockStart == -1 {
err = errors.New(errors.MissingBlockStart, nil, tokens[0].End())
return
}
if blockEnd == -1 {
err = errors.New(errors.MissingBlockEnd, nil, tokens[len(tokens)-1].End())
return
}
body, err = Parse(tokens[blockStart+1:blockEnd], source)
return
}

View File

@ -2,18 +2,16 @@ package core
import ( import (
"git.akyoto.dev/cli/q/src/ast" "git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token" "git.akyoto.dev/cli/q/src/token"
) )
// CompileTokens compiles a token list. // CompileTokens compiles a token list.
func (f *Function) CompileTokens(tokens []token.Token) error { func (f *Function) CompileTokens(tokens []token.Token) error {
body, err := ast.Parse(tokens, f.File.Bytes) tree, err := ast.Parse(tokens, f.File)
if err != nil { if err != nil {
err.(*errors.Error).File = f.File
return err return err
} }
return f.CompileAST(body) return f.CompileAST(tree)
} }