diff --git a/src/build/ast/Parse.go b/src/build/ast/Parse.go index 4db7cca..2c6fd94 100644 --- a/src/build/ast/Parse.go +++ b/src/build/ast/Parse.go @@ -1,17 +1,16 @@ 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" ) // 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) err := EachInstruction(tokens, func(instruction token.List) error { - node, err := toASTNode(instruction, buffer) + node, err := parseNode(instruction, source) if err == nil && node != nil { tree = append(tree, node) @@ -23,83 +22,6 @@ func Parse(tokens []token.Token, buffer []byte) (AST, error) { 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. func IsAssignment(expr *expression.Expression) bool { return expr.Token.IsAssignment() @@ -114,8 +36,3 @@ func IsFunctionCall(expr *expression.Expression) bool { func IsVariableDefinition(expr *expression.Expression) bool { 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 -} diff --git a/src/build/ast/parseKeyword.go b/src/build/ast/parseKeyword.go new file mode 100644 index 0000000..f8aa676 --- /dev/null +++ b/src/build/ast/parseKeyword.go @@ -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 +} diff --git a/src/build/ast/parseNode.go b/src/build/ast/parseNode.go new file mode 100644 index 0000000..9ea7174 --- /dev/null +++ b/src/build/ast/parseNode.go @@ -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) + } +} diff --git a/src/build/expression/Expression.go b/src/build/expression/Expression.go index 057476b..070b4fa 100644 --- a/src/build/expression/Expression.go +++ b/src/build/expression/Expression.go @@ -114,16 +114,16 @@ func (expr *Expression) LastChild() *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{} - expr.write(&builder, data) + expr.write(&builder, source) return builder.String() } // 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() { - builder.WriteString(expr.Token.Text(data)) + builder.WriteString(expr.Token.Text(source)) return } @@ -135,12 +135,12 @@ func (expr *Expression) write(builder *strings.Builder, data []byte) { case token.Array: builder.WriteString("@") default: - builder.WriteString(expr.Token.Text(data)) + builder.WriteString(expr.Token.Text(source)) } for _, child := range expr.Children { builder.WriteByte(' ') - child.write(builder, data) + child.write(builder, source) } builder.WriteByte(')')