From b6559505169948404d44a1fc58ceba84f88e4ee5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 20 Feb 2025 14:12:25 +0100 Subject: [PATCH] Simplified ast package --- src/ast/AST.go | 56 +++++++++++++++++++ src/ast/Assert.go | 10 ---- src/ast/Assign.go | 10 ---- src/ast/Call.go | 8 --- src/ast/Count.go | 4 ++ src/ast/Define.go | 10 ---- src/ast/For.go | 9 --- src/ast/If.go | 12 ---- src/ast/Loop.go | 6 -- src/ast/Parse.go | 25 ++------- src/ast/Return.go | 10 ---- src/ast/Switch.go | 16 ------ src/ast/block.go | 26 +++++++++ ...{EachInstruction.go => eachInstruction.go} | 14 ++--- src/ast/helpers.go | 21 +++++++ src/ast/{parseSwitch.go => parseCases.go} | 11 ++-- src/ast/{parseNode.go => parseInstruction.go} | 19 ++++--- src/ast/parseKeyword.go | 46 +++++---------- src/core/CompileTokens.go | 6 +- 19 files changed, 151 insertions(+), 168 deletions(-) delete mode 100644 src/ast/Assert.go delete mode 100644 src/ast/Assign.go delete mode 100644 src/ast/Call.go delete mode 100644 src/ast/Define.go delete mode 100644 src/ast/For.go delete mode 100644 src/ast/If.go delete mode 100644 src/ast/Loop.go delete mode 100644 src/ast/Return.go delete mode 100644 src/ast/Switch.go create mode 100644 src/ast/block.go rename src/ast/{EachInstruction.go => eachInstruction.go} (69%) create mode 100644 src/ast/helpers.go rename src/ast/{parseSwitch.go => parseCases.go} (60%) rename src/ast/{parseNode.go => parseInstruction.go} (59%) diff --git a/src/ast/AST.go b/src/ast/AST.go index 8dcaa24..99f1ed1 100644 --- a/src/ast/AST.go +++ b/src/ast/AST.go @@ -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 +} diff --git a/src/ast/Assert.go b/src/ast/Assert.go deleted file mode 100644 index 6da47fb..0000000 --- a/src/ast/Assert.go +++ /dev/null @@ -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 -} diff --git a/src/ast/Assign.go b/src/ast/Assign.go deleted file mode 100644 index 8bdbe99..0000000 --- a/src/ast/Assign.go +++ /dev/null @@ -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 -} diff --git a/src/ast/Call.go b/src/ast/Call.go deleted file mode 100644 index 8aa683e..0000000 --- a/src/ast/Call.go +++ /dev/null @@ -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 -} diff --git a/src/ast/Count.go b/src/ast/Count.go index 2c9f1f7..83e7f69 100644 --- a/src/ast/Count.go +++ b/src/ast/Count.go @@ -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) diff --git a/src/ast/Define.go b/src/ast/Define.go deleted file mode 100644 index 0b8030c..0000000 --- a/src/ast/Define.go +++ /dev/null @@ -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 -} diff --git a/src/ast/For.go b/src/ast/For.go deleted file mode 100644 index 32a080b..0000000 --- a/src/ast/For.go +++ /dev/null @@ -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 -} diff --git a/src/ast/If.go b/src/ast/If.go deleted file mode 100644 index 079640d..0000000 --- a/src/ast/If.go +++ /dev/null @@ -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 -} diff --git a/src/ast/Loop.go b/src/ast/Loop.go deleted file mode 100644 index 8bf3fac..0000000 --- a/src/ast/Loop.go +++ /dev/null @@ -1,6 +0,0 @@ -package ast - -// Loop is a block of infinitely repeating instructions. -type Loop struct { - Body AST -} diff --git a/src/ast/Parse.go b/src/ast/Parse.go index 8b9083e..84c9972 100644 --- a/src/ast/Parse.go +++ b/src/ast/Parse.go @@ -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 -} diff --git a/src/ast/Return.go b/src/ast/Return.go deleted file mode 100644 index 8815638..0000000 --- a/src/ast/Return.go +++ /dev/null @@ -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 -} diff --git a/src/ast/Switch.go b/src/ast/Switch.go deleted file mode 100644 index a5fc6a0..0000000 --- a/src/ast/Switch.go +++ /dev/null @@ -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 -} diff --git a/src/ast/block.go b/src/ast/block.go new file mode 100644 index 0000000..da58e2a --- /dev/null +++ b/src/ast/block.go @@ -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 +} diff --git a/src/ast/EachInstruction.go b/src/ast/eachInstruction.go similarity index 69% rename from src/ast/EachInstruction.go rename to src/ast/eachInstruction.go index b1a354f..ceb653f 100644 --- a/src/ast/EachInstruction.go +++ b/src/ast/eachInstruction.go @@ -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 diff --git a/src/ast/helpers.go b/src/ast/helpers.go new file mode 100644 index 0000000..ffb2bf5 --- /dev/null +++ b/src/ast/helpers.go @@ -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 +} diff --git a/src/ast/parseSwitch.go b/src/ast/parseCases.go similarity index 60% rename from src/ast/parseSwitch.go rename to src/ast/parseCases.go index 65eb712..da6e561 100644 --- a/src/ast/parseSwitch.go +++ b/src/ast/parseCases.go @@ -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) diff --git a/src/ast/parseNode.go b/src/ast/parseInstruction.go similarity index 59% rename from src/ast/parseNode.go rename to src/ast/parseInstruction.go index 919566e..c14db17 100644 --- a/src/ast/parseNode.go +++ b/src/ast/parseInstruction.go @@ -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) } } diff --git a/src/ast/parseKeyword.go b/src/ast/parseKeyword.go index a0a4d45..c012e38 100644 --- a/src/ast/parseKeyword.go +++ b/src/ast/parseKeyword.go @@ -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 -} diff --git a/src/core/CompileTokens.go b/src/core/CompileTokens.go index 06bff0f..2aa390c 100644 --- a/src/core/CompileTokens.go +++ b/src/core/CompileTokens.go @@ -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) }