Improved AST parser

This commit is contained in:
Eduard Urbach 2024-07-30 12:48:48 +02:00
parent 265ab988d9
commit 1e7a1399d3
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
4 changed files with 109 additions and 91 deletions

View File

@ -1,17 +1,16 @@
package ast package ast
import ( import (
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/token" "git.akyoto.dev/cli/q/src/build/token"
) )
// Parse generates an AST from a list of tokens. // Parse generates an AST from a list of tokens.
func Parse(tokens []token.Token, buffer []byte) (AST, error) { func Parse(tokens []token.Token, source []byte) (AST, error) {
tree := make(AST, 0, len(tokens)/64) tree := make(AST, 0, len(tokens)/64)
err := EachInstruction(tokens, func(instruction token.List) error { err := EachInstruction(tokens, func(instruction token.List) error {
node, err := toASTNode(instruction, buffer) node, err := parseNode(instruction, source)
if err == nil && node != nil { if err == nil && node != nil {
tree = append(tree, node) tree = append(tree, node)
@ -23,83 +22,6 @@ func Parse(tokens []token.Token, buffer []byte) (AST, error) {
return tree, err return tree, err
} }
// toASTNode generates an AST node from an instruction.
func toASTNode(tokens token.List, buffer []byte) (Node, error) {
if tokens[0].IsKeyword() {
if tokens[0].Kind == token.Return {
if len(tokens) == 1 {
return &Return{}, nil
}
value := expression.Parse(tokens[1:])
return &Return{Value: value}, nil
}
if tokens[0].Kind == token.Assert {
if len(tokens) == 1 {
return nil, errors.New(errors.MissingExpression, nil, tokens[0].End())
}
condition := expression.Parse(tokens[1:])
return &Assert{Condition: condition}, nil
}
if keywordHasBlock(tokens[0].Kind) {
blockStart := tokens.IndexKind(token.BlockStart)
blockEnd := tokens.LastIndexKind(token.BlockEnd)
if blockStart == -1 {
return nil, errors.New(errors.MissingBlockStart, nil, tokens[0].End())
}
if blockEnd == -1 {
return nil, errors.New(errors.MissingBlockEnd, nil, tokens[len(tokens)-1].End())
}
body, err := Parse(tokens[blockStart+1:blockEnd], buffer)
switch tokens[0].Kind {
case token.If:
condition := expression.Parse(tokens[1:blockStart])
return &If{Condition: condition, Body: body}, err
case token.Loop:
return &Loop{Body: body}, err
}
}
return nil, errors.New(&errors.KeywordNotImplemented{Keyword: tokens[0].Text(buffer)}, nil, tokens[0].Position)
}
expr := expression.Parse(tokens)
if expr == nil {
return nil, nil
}
switch {
case IsVariableDefinition(expr):
if len(expr.Children) < 2 {
return nil, errors.New(errors.MissingOperand, nil, expr.Token.End())
}
return &Define{Expression: expr}, nil
case IsAssignment(expr):
if len(expr.Children) < 2 {
return nil, errors.New(errors.MissingOperand, nil, expr.Token.End())
}
return &Assign{Expression: expr}, nil
case IsFunctionCall(expr):
return &Call{Expression: expr}, nil
default:
return nil, errors.New(&errors.InvalidInstruction{Instruction: expr.Token.Text(buffer)}, nil, expr.Token.Position)
}
}
// IsAssignment returns true if the expression is an assignment. // IsAssignment returns true if the expression is an assignment.
func IsAssignment(expr *expression.Expression) bool { func IsAssignment(expr *expression.Expression) bool {
return expr.Token.IsAssignment() return expr.Token.IsAssignment()
@ -114,8 +36,3 @@ func IsFunctionCall(expr *expression.Expression) bool {
func IsVariableDefinition(expr *expression.Expression) bool { func IsVariableDefinition(expr *expression.Expression) bool {
return expr.Token.Kind == token.Define return expr.Token.Kind == token.Define
} }
// keywordHasBlock returns true if the keyword requires a block.
func keywordHasBlock(kind token.Kind) bool {
return kind == token.If || kind == token.Loop
}

View File

@ -0,0 +1,59 @@
package ast
import (
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/token"
)
// parseKeyword generates a keyword node from an instruction.
func parseKeyword(tokens token.List, source []byte) (Node, error) {
switch tokens[0].Kind {
case token.Assert:
if len(tokens) == 1 {
return nil, errors.New(errors.MissingExpression, nil, tokens[0].End())
}
condition := expression.Parse(tokens[1:])
return &Assert{Condition: condition}, nil
case token.If:
blockStart, _, body, err := block(tokens, source)
condition := expression.Parse(tokens[1:blockStart])
return &If{Condition: condition, Body: body}, err
case token.Loop:
_, _, body, err := block(tokens, source)
return &Loop{Body: body}, err
case token.Return:
if len(tokens) == 1 {
return &Return{}, nil
}
value := expression.Parse(tokens[1:])
return &Return{Value: value}, nil
default:
return nil, errors.New(&errors.KeywordNotImplemented{Keyword: tokens[0].Text(source)}, nil, 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

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

View File

@ -114,16 +114,16 @@ func (expr *Expression) LastChild() *Expression {
} }
// String generates a textual representation of the expression. // String generates a textual representation of the expression.
func (expr *Expression) String(data []byte) string { func (expr *Expression) String(source []byte) string {
builder := strings.Builder{} builder := strings.Builder{}
expr.write(&builder, data) expr.write(&builder, source)
return builder.String() return builder.String()
} }
// write generates a textual representation of the expression. // write generates a textual representation of the expression.
func (expr *Expression) write(builder *strings.Builder, data []byte) { func (expr *Expression) write(builder *strings.Builder, source []byte) {
if expr.IsLeaf() { if expr.IsLeaf() {
builder.WriteString(expr.Token.Text(data)) builder.WriteString(expr.Token.Text(source))
return return
} }
@ -135,12 +135,12 @@ func (expr *Expression) write(builder *strings.Builder, data []byte) {
case token.Array: case token.Array:
builder.WriteString("@") builder.WriteString("@")
default: default:
builder.WriteString(expr.Token.Text(data)) builder.WriteString(expr.Token.Text(source))
} }
for _, child := range expr.Children { for _, child := range expr.Children {
builder.WriteByte(' ') builder.WriteByte(' ')
child.write(builder, data) child.write(builder, source)
} }
builder.WriteByte(')') builder.WriteByte(')')