Simplified ast package
This commit is contained in:
parent
45a36a645a
commit
b655950516
@ -1,7 +1,63 @@
|
||||
package ast
|
||||
|
||||
import "git.akyoto.dev/cli/q/src/expression"
|
||||
|
||||
// Node is an interface used for all types of AST nodes.
|
||||
type Node any
|
||||
|
||||
// AST is an abstract syntax tree which is simply a list of nodes.
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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.Else, buffer, kind, name)
|
||||
|
||||
case *For:
|
||||
count += node.Head.Count(buffer, kind, name)
|
||||
count += Count(node.Body, buffer, kind, name)
|
||||
|
||||
case *Loop:
|
||||
count += Count(node.Body, buffer, kind, name)
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package ast
|
||||
|
||||
// Loop is a block of infinitely repeating instructions.
|
||||
type Loop struct {
|
||||
Body AST
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"git.akyoto.dev/cli/q/src/expression"
|
||||
"git.akyoto.dev/cli/q/src/fs"
|
||||
"git.akyoto.dev/cli/q/src/token"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
err := EachInstruction(tokens, func(instruction token.List) error {
|
||||
node, err := parseNode(instruction, source, nodes)
|
||||
err := eachInstruction(tokens, func(instruction token.List) error {
|
||||
node, err := parseInstruction(instruction, file, nodes)
|
||||
|
||||
if err == nil && node != nil {
|
||||
if node != nil {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
@ -21,18 +21,3 @@ func Parse(tokens []token.Token, source []byte) (AST, error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
26
src/ast/block.go
Normal 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
|
||||
}
|
@ -2,13 +2,13 @@ package ast
|
||||
|
||||
import "git.akyoto.dev/cli/q/src/token"
|
||||
|
||||
// EachInstruction calls the function on each instruction.
|
||||
func EachInstruction(body token.List, call func(token.List) error) error {
|
||||
// eachInstruction calls the function on each AST node.
|
||||
func eachInstruction(tokens token.List, call func(token.List) error) error {
|
||||
start := 0
|
||||
groupLevel := 0
|
||||
blockLevel := 0
|
||||
|
||||
for i, t := range body {
|
||||
for i, t := range tokens {
|
||||
if start == i && t.Kind == token.NewLine {
|
||||
start = i + 1
|
||||
continue
|
||||
@ -20,7 +20,7 @@ func EachInstruction(body token.List, call func(token.List) error) error {
|
||||
continue
|
||||
}
|
||||
|
||||
err := call(body[start:i])
|
||||
err := call(tokens[start:i])
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -44,7 +44,7 @@ func EachInstruction(body token.List, call func(token.List) error) error {
|
||||
continue
|
||||
}
|
||||
|
||||
err := call(body[start : i+1])
|
||||
err := call(tokens[start : i+1])
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -54,8 +54,8 @@ func EachInstruction(body token.List, call func(token.List) error) error {
|
||||
}
|
||||
}
|
||||
|
||||
if start != len(body) {
|
||||
return call(body[start:])
|
||||
if start != len(tokens) {
|
||||
return call(tokens[start:])
|
||||
}
|
||||
|
||||
return nil
|
21
src/ast/helpers.go
Normal file
21
src/ast/helpers.go
Normal 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
|
||||
}
|
@ -2,15 +2,16 @@ package ast
|
||||
|
||||
import (
|
||||
"git.akyoto.dev/cli/q/src/expression"
|
||||
"git.akyoto.dev/cli/q/src/fs"
|
||||
"git.akyoto.dev/cli/q/src/token"
|
||||
)
|
||||
|
||||
// parseSwitch generates the cases inside a switch statement.
|
||||
func parseSwitch(tokens token.List, source []byte) ([]Case, error) {
|
||||
// parseCases generates the cases inside a switch statement.
|
||||
func parseCases(tokens token.List, file *fs.File) ([]Case, error) {
|
||||
var cases []Case
|
||||
|
||||
err := EachInstruction(tokens, func(caseTokens token.List) error {
|
||||
blockStart, _, body, err := block(caseTokens, source)
|
||||
err := eachInstruction(tokens, func(caseTokens token.List) error {
|
||||
blockStart, _, body, err := block(caseTokens, file)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -19,7 +20,7 @@ func parseSwitch(tokens token.List, source []byte) ([]Case, error) {
|
||||
conditionTokens := caseTokens[:blockStart]
|
||||
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
|
||||
} else {
|
||||
condition = expression.Parse(conditionTokens)
|
@ -3,13 +3,14 @@ package ast
|
||||
import (
|
||||
"git.akyoto.dev/cli/q/src/errors"
|
||||
"git.akyoto.dev/cli/q/src/expression"
|
||||
"git.akyoto.dev/cli/q/src/fs"
|
||||
"git.akyoto.dev/cli/q/src/token"
|
||||
)
|
||||
|
||||
// parseNode generates an AST node from an instruction.
|
||||
func parseNode(tokens token.List, source []byte, nodes AST) (Node, error) {
|
||||
// parseInstruction generates an AST node from an instruction.
|
||||
func parseInstruction(tokens token.List, file *fs.File, nodes AST) (Node, error) {
|
||||
if tokens[0].IsKeyword() {
|
||||
return parseKeyword(tokens, source, nodes)
|
||||
return parseKeyword(tokens, file, nodes)
|
||||
}
|
||||
|
||||
expr := expression.Parse(tokens)
|
||||
@ -19,24 +20,24 @@ func parseNode(tokens token.List, source []byte, nodes AST) (Node, error) {
|
||||
}
|
||||
|
||||
switch {
|
||||
case IsFunctionCall(expr):
|
||||
return &Call{Expression: expr}, nil
|
||||
|
||||
case IsVariableDefinition(expr):
|
||||
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
|
||||
|
||||
case IsAssignment(expr):
|
||||
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
|
||||
|
||||
case IsFunctionCall(expr):
|
||||
return &Call{Expression: expr}, nil
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -3,44 +3,45 @@ package ast
|
||||
import (
|
||||
"git.akyoto.dev/cli/q/src/errors"
|
||||
"git.akyoto.dev/cli/q/src/expression"
|
||||
"git.akyoto.dev/cli/q/src/fs"
|
||||
"git.akyoto.dev/cli/q/src/token"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
case token.Assert:
|
||||
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:])
|
||||
return &Assert{Condition: condition}, nil
|
||||
|
||||
case token.If:
|
||||
blockStart, _, body, err := block(tokens, source)
|
||||
blockStart, _, body, err := block(tokens, file)
|
||||
condition := expression.Parse(tokens[1:blockStart])
|
||||
return &If{Condition: condition, Body: body}, err
|
||||
|
||||
case token.Else:
|
||||
_, _, body, err := block(tokens, source)
|
||||
_, _, body, err := block(tokens, file)
|
||||
|
||||
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]
|
||||
ifNode, exists := last.(*If)
|
||||
|
||||
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
|
||||
return nil, err
|
||||
|
||||
case token.For:
|
||||
blockStart, _, body, err := block(tokens, source)
|
||||
blockStart, _, body, err := block(tokens, file)
|
||||
head := tokens[1:blockStart]
|
||||
|
||||
loop := &For{
|
||||
@ -51,7 +52,7 @@ func parseKeyword(tokens token.List, source []byte, nodes AST) (Node, error) {
|
||||
return loop, err
|
||||
|
||||
case token.Loop:
|
||||
_, _, body, err := block(tokens, source)
|
||||
_, _, body, err := block(tokens, file)
|
||||
return &Loop{Body: body}, err
|
||||
|
||||
case token.Return:
|
||||
@ -67,42 +68,23 @@ func parseKeyword(tokens token.List, source []byte, nodes AST) (Node, error) {
|
||||
blockEnd := tokens.LastIndexKind(token.BlockEnd)
|
||||
|
||||
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 {
|
||||
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]
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -2,18 +2,16 @@ package core
|
||||
|
||||
import (
|
||||
"git.akyoto.dev/cli/q/src/ast"
|
||||
"git.akyoto.dev/cli/q/src/errors"
|
||||
"git.akyoto.dev/cli/q/src/token"
|
||||
)
|
||||
|
||||
// CompileTokens compiles a token list.
|
||||
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 {
|
||||
err.(*errors.Error).File = f.File
|
||||
return err
|
||||
}
|
||||
|
||||
return f.CompileAST(body)
|
||||
return f.CompileAST(tree)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user