├── go.sum ├── .gitignore ├── go.mod ├── .travis.yml ├── ast ├── ast_test.go └── ast.go ├── parser ├── parser_test.go └── parser.go ├── LICENSE ├── cmd └── parse │ ├── parse.go │ └── parse_test.go ├── scanner ├── scanner_test.go └── scanner.go ├── token └── token.go └── README.md /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/richardjennings/prattparser 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - "1.11" 5 | env: 6 | - GO111MODULE=on 7 | script: 8 | - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) 11 | -------------------------------------------------------------------------------- /ast/ast_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "github.com/richardjennings/prattparser/token" 6 | "testing" 7 | ) 8 | 9 | func TestAstTreeStringer(t *testing.T) { 10 | expr := BinaryExpr{ 11 | X: UnaryExpr{ 12 | X: ScalarExpr{Val: "1", Typ: token.INT}, 13 | Op: token.SUB, 14 | }, 15 | Op: token.MUL, 16 | Y: BinaryExpr{ 17 | X: UnaryExpr{ 18 | X: ScalarExpr{Val: "2", Typ: token.INT}, 19 | Op: token.ADD, 20 | }, 21 | Op: token.MUL, 22 | Y: UnaryExpr{ 23 | X: ScalarExpr{Val: "2", Typ: token.INT}, 24 | Op: token.SUB, 25 | }, 26 | }, 27 | } 28 | 29 | expected := "(-1 * (+2 * -2))" 30 | actual := fmt.Sprintf("%s", expr) 31 | 32 | if actual != expected { 33 | t.Errorf("expected %s got %s", expected, actual) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func errorMsg(t *testing.T, expected string, actual string) { 9 | t.Errorf("expected %s, got %s", expected, actual) 10 | } 11 | 12 | func TestParser_Parse(t *testing.T) { 13 | type testCase struct { 14 | src string 15 | expect string 16 | } 17 | 18 | cases := []testCase{ 19 | {"-1", "-1"}, 20 | {"+1", "+1"}, 21 | {"1 + 2", "(1 + 2)"}, 22 | {"1 + 2 * 3", "(1 + (2 * 3))"}, 23 | {"1 + 2 * 3 - 1", "((1 + (2 * 3)) - 1)"}, 24 | {"1 / 2 * 3 - 1", "(((1 / 2) * 3) - 1)"}, 25 | {"3 * 5 % 2 + 1", "((3 * (5 % 2)) + 1)"}, 26 | {"( 1 + 2 ) * 3", "((1 + 2) * 3)"}, 27 | {"5 ^ 4 ^ 9", "(5 ^ (4 ^ 9))"}, 28 | } 29 | 30 | for _, tcase := range cases { 31 | p := NewParser(tcase.src) 32 | actual := fmt.Sprint(p.Parse()) 33 | if actual != tcase.expect { 34 | errorMsg(t, tcase.expect, actual) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Richard Jennings 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | // Package ast provides AST representations and stringer methods 2 | package ast 3 | 4 | import ( 5 | "fmt" 6 | "github.com/richardjennings/prattparser/token" 7 | ) 8 | 9 | type ( 10 | // Expr interface for AST Expressions 11 | Expr interface{} 12 | 13 | // ScalarExpr represents Scalar Expressions such as a Integer 14 | ScalarExpr struct { 15 | Expr 16 | Val string 17 | Typ token.Token 18 | } 19 | 20 | // UnaryExpr represents Unary Expressions such as - 1 21 | UnaryExpr struct { 22 | Expr 23 | Op token.Token 24 | X Expr 25 | } 26 | 27 | // BinaryExpr represents Binary Expression such as 1 + 1 28 | BinaryExpr struct { 29 | Expr 30 | X Expr 31 | Op token.Token 32 | Y Expr 33 | } 34 | ) 35 | 36 | // String representation of Scalar Expression 37 | func (s ScalarExpr) String() string { 38 | return fmt.Sprintf("%s", s.Val) 39 | } 40 | 41 | // String representation of Unary Expression 42 | func (s UnaryExpr) String() string { 43 | return fmt.Sprintf("%s%s", s.Op, s.X) 44 | } 45 | 46 | // String representation of Binary Expression 47 | func (s BinaryExpr) String() string { 48 | return fmt.Sprintf("(%s %s %s)", s.X, s.Op, s.Y) 49 | } 50 | -------------------------------------------------------------------------------- /cmd/parse/parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/richardjennings/prattparser/parser" 6 | "io" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // cli struct provides a configurable io.Writer for exec 12 | type cli struct { 13 | o io.Writer 14 | e io.Writer 15 | exitFn func(int int) 16 | } 17 | 18 | // NewCli returns a new cli pointer using the supplied io.Writer implementations 19 | func NewCli(out io.Writer, err io.Writer, exit func(int int)) *cli { 20 | return &cli{o: out, e: err, exitFn: exit} 21 | } 22 | 23 | // A Command Line Interface to parse expressions 24 | func main() { 25 | NewCli(os.Stdout, os.Stderr, os.Exit).exec(os.Args) 26 | } 27 | 28 | // exec executes the parser with supplied src, formats the result and writes to cli io.Writer 29 | func (c *cli) exec(args []string) { 30 | if len(args) != 2 { 31 | fmt.Fprintln(c.e, `example usage: ./parse "1 + 1"`) 32 | c.exitFn(1) 33 | return 34 | } 35 | defer func() { 36 | if r := recover(); r != nil { 37 | fmt.Fprintln(c.e, r) 38 | } 39 | }() 40 | p := parser.NewParser(args[1]) 41 | 42 | s := fmt.Sprintf("%s", p.Parse()) 43 | s = strings.TrimPrefix(s, "(") 44 | s = strings.TrimSuffix(s, ")") 45 | fmt.Fprintln(c.o, s) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/parse/parse_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestExec_missing_argument(t *testing.T) { 9 | expected := `example usage: ./parse "1 + 1" 10 | ` 11 | bufo := bytes.NewBuffer([]byte{}) 12 | bufe := bytes.NewBuffer([]byte{}) 13 | cli := NewCli(bufo, bufe, func(int int) {}) 14 | cli.exec([]string{}) 15 | actual := bufe.String() 16 | if actual != expected { 17 | t.Errorf("expected %v got %v", expected, actual) 18 | } 19 | } 20 | 21 | func TestExec_invalid_token(t *testing.T) { 22 | src := "1 + t" 23 | expected := "Parse Error: t\n" 24 | bufo := bytes.NewBuffer([]byte{}) 25 | bufe := bytes.NewBuffer([]byte{}) 26 | cli := NewCli(bufo, bufe, func(int int) {}) 27 | cli.exec([]string{"", src}) 28 | actual := bufe.String() 29 | if actual != expected { 30 | t.Errorf("expected %v got %v", expected, actual) 31 | } 32 | } 33 | 34 | func TestExec_missing_rparen(t *testing.T) { 35 | src := "(1 + 2" 36 | expected := "Parse Error: expected )\n" 37 | bufo := bytes.NewBuffer([]byte{}) 38 | bufe := bytes.NewBuffer([]byte{}) 39 | cli := NewCli(bufo, bufe, func(int int) {}) 40 | cli.exec([]string{"", src}) 41 | actual := bufe.String() 42 | if actual != expected { 43 | t.Errorf("expected %v got %v", expected, actual) 44 | } 45 | } 46 | 47 | func TestExec(t *testing.T) { 48 | src := "3 * 2 ^ 4 + (7 / 2) - 1" 49 | expected := "((3 * (2 ^ 4)) + (7 / 2)) - 1\n" 50 | bufo := bytes.NewBuffer([]byte{}) 51 | bufe := bytes.NewBuffer([]byte{}) 52 | cli := NewCli(bufo, bufe, func(int int) {}) 53 | cli.exec([]string{"", src}) 54 | actual := bufo.String() 55 | if actual != expected { 56 | t.Errorf("expected %v got %v", expected, actual) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scanner/scanner_test.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "bytes" 5 | "github.com/richardjennings/prattparser/token" 6 | "testing" 7 | ) 8 | 9 | type testCase struct { 10 | Tok token.Token 11 | Lit string 12 | } 13 | 14 | func errorMsg(t *testing.T, expected testCase, actual Lexed) { 15 | t.Errorf("expected %v, got %v", expected, actual) 16 | } 17 | 18 | func TestInterpLexer_Lex_eof(t *testing.T) { 19 | s := Scanner{Src: bytes.NewBuffer([]byte(``))} 20 | if lexed := s.Lex(); lexed.Tok != token.EOF { 21 | t.Error(lexed) 22 | } 23 | } 24 | 25 | func TestInterpLexer_Lex_illegal(t *testing.T) { 26 | s := Scanner{Src: bytes.NewBuffer([]byte(`a`))} 27 | if lexed := s.Lex(); lexed.Tok != token.ILLEGAL || lexed.Lit != "a" { 28 | t.Error(lexed) 29 | } 30 | } 31 | 32 | func TestInterpLexer_Lex_int(t *testing.T) { 33 | s := Scanner{Src: bytes.NewBuffer([]byte(`0 10 1009 +1`))} 34 | expected := [...]testCase{ 35 | {token.INT, "0"}, 36 | {token.INT, "10"}, 37 | {token.INT, "1009"}, 38 | {token.ADD, ""}, 39 | {token.INT, "1"}, 40 | } 41 | for _, expect := range expected { 42 | lexed := s.Lex() 43 | if lexed.Tok != expect.Tok || lexed.Lit != expect.Lit { 44 | errorMsg(t, expect, lexed) 45 | } 46 | } 47 | } 48 | 49 | func TestInterpLexer_Lex_operator(t *testing.T) { 50 | s := Scanner{Src: bytes.NewBuffer([]byte(`+-*/%^()`))} 51 | expected := [...]testCase{ 52 | {token.ADD, ""}, 53 | {token.SUB, ""}, 54 | {token.MUL, ""}, 55 | {token.QUO, ""}, 56 | {token.REM, ""}, 57 | {token.POW, ""}, 58 | {token.LPAREN, ""}, 59 | {token.RPAREN, ""}, 60 | } 61 | for _, expect := range expected { 62 | lexed := s.Lex() 63 | if lexed.Tok != expect.Tok || lexed.Lit != expect.Lit { 64 | errorMsg(t, expect, lexed) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scanner/scanner.go: -------------------------------------------------------------------------------- 1 | // Package scanner provides a UTF8 scanner and Lexer 2 | package scanner 3 | 4 | import ( 5 | "bytes" 6 | "github.com/richardjennings/prattparser/token" 7 | ) 8 | 9 | // The Scanner struct containing a bytes buffer and the 10 | // next rune if not consumed by pass 11 | type Scanner struct { 12 | Src *bytes.Buffer 13 | nch rune 14 | } 15 | 16 | // A Lexed struct contains a Lexed Token and Literal 17 | type Lexed struct { 18 | Tok token.Token 19 | Lit string 20 | } 21 | 22 | // Scan returns the next rune or 0 on EOF 23 | func (s *Scanner) Scan() rune { 24 | var ch rune 25 | var err error 26 | if s.nch != 0 { 27 | ch = s.nch 28 | s.nch = 0 29 | } else if ch, _, err = s.Src.ReadRune(); err != nil { 30 | return 0 31 | } 32 | 33 | return ch 34 | } 35 | 36 | // Lex skips whitespace and returns a Lexed struct containing a token.Token, string pair 37 | func (s *Scanner) Lex() (lexed Lexed) { 38 | ch := s.Scan() 39 | // skip whitespace 40 | for ch == ' ' || ch == '\n' || ch == '\t' { 41 | ch = s.Scan() 42 | } 43 | switch true { 44 | case '0' <= ch && ch <= '9': 45 | // int 46 | buf := []rune{ch} 47 | ch = s.Scan() 48 | for ch >= '0' && ch <= '9' { 49 | buf = append(buf, ch) 50 | ch = s.Scan() 51 | } 52 | s.nch = ch 53 | lexed.Tok = token.INT 54 | lexed.Lit = string(buf) 55 | default: 56 | switch ch { 57 | case 0: 58 | lexed.Tok = token.EOF 59 | case '+': 60 | lexed.Tok = token.ADD 61 | case '-': 62 | lexed.Tok = token.SUB 63 | case '*': 64 | lexed.Tok = token.MUL 65 | case '/': 66 | lexed.Tok = token.QUO 67 | case '%': 68 | lexed.Tok = token.REM 69 | case '^': 70 | lexed.Tok = token.POW 71 | case '(': 72 | lexed.Tok = token.LPAREN 73 | case ')': 74 | lexed.Tok = token.RPAREN 75 | default: 76 | lexed.Tok = token.ILLEGAL 77 | lexed.Lit = string(ch) 78 | } 79 | } 80 | 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | // Package parser provides an implementation of Pratt Top-down Operator Precedence parsing 2 | package parser 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "github.com/richardjennings/prattparser/ast" 8 | "github.com/richardjennings/prattparser/scanner" 9 | "github.com/richardjennings/prattparser/token" 10 | ) 11 | 12 | // The Parser struct 13 | type Parser struct { 14 | Scanner scanner.Scanner 15 | lexed scanner.Lexed 16 | } 17 | 18 | // NewParser creates a new parser 19 | func NewParser(src string) *Parser { 20 | return &Parser{Scanner: scanner.Scanner{Src: bytes.NewBuffer([]byte(src))}} 21 | } 22 | 23 | // Parse Lexed tokens returning an Abstract Syntax Treee 24 | func (p *Parser) Parse() ast.Expr { 25 | p.lexed = p.Scanner.Lex() 26 | return p.expr(token.LowestPrec) 27 | } 28 | 29 | // Implementation of Pratt Precedence 30 | func (p *Parser) expr(rbp int) ast.Expr { 31 | t := p.lexed 32 | p.lexed = p.Scanner.Lex() 33 | 34 | //null denotation 35 | var left interface{} 36 | switch t.Tok { 37 | case token.ILLEGAL: 38 | panic(fmt.Sprintf("Parse Error: %s", t.Lit)) 39 | case token.LPAREN: 40 | left = p.expr(0) 41 | if p.lexed.Tok != token.RPAREN { 42 | panic("Parse Error: expected )") 43 | } 44 | p.lexed = p.Scanner.Lex() 45 | default: 46 | switch true { 47 | case t.Tok.IsScalar(): 48 | left = ast.ScalarExpr{Val: t.Lit, Typ: t.Tok} 49 | case t.Tok.IsUnary(): 50 | left = ast.UnaryExpr{X: p.expr(token.HighestPrec), Op: t.Tok} 51 | } 52 | } 53 | 54 | // left binding power 55 | for rbp < p.lexed.Tok.Precedence() { 56 | t = p.lexed 57 | p.lexed = p.Scanner.Lex() 58 | //left denotation 59 | switch true { 60 | case t.Tok.IsRightAssoc(): 61 | left = ast.BinaryExpr{X: left, Op: t.Tok, Y: p.expr(t.Tok.Precedence() - 1)} 62 | case t.Tok.IsLeftAssoc(): 63 | left = ast.BinaryExpr{X: left, Op: t.Tok, Y: p.expr(t.Tok.Precedence())} 64 | } 65 | } 66 | return left 67 | } 68 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | // Package token provides a representation of Lexed tokens 2 | package token 3 | 4 | // The Token type 5 | type Token int 6 | 7 | // Individual Token constants 8 | const ( 9 | ILLEGAL Token = iota 10 | EOF 11 | INT // 12345 12 | ADD // + 13 | SUB // - 14 | MUL // * 15 | QUO // / 16 | REM // % 17 | POW // ^ 18 | LPAREN // ( 19 | RPAREN // ) 20 | ) 21 | 22 | // String representations of Token constants 23 | var tokens = [...]string{ 24 | ILLEGAL: "illegal", 25 | EOF: "EOF", 26 | INT: "int", 27 | ADD: "+", 28 | SUB: "-", 29 | MUL: "*", 30 | QUO: "/", 31 | REM: "%", 32 | POW: "^", 33 | LPAREN: "(", 34 | RPAREN: ")", 35 | } 36 | 37 | // Representation of highest and lowest precedence 38 | const ( 39 | LowestPrec = 0 40 | HighestPrec = 7 41 | ) 42 | 43 | // Precedence returns a tokens precedence if defined, otherwise LowestPrec 44 | func (tok Token) Precedence() int { 45 | switch tok { 46 | case ADD, SUB: 47 | return 4 48 | case MUL, QUO: 49 | return 5 50 | case POW: 51 | return 6 52 | case REM: 53 | return 7 54 | } 55 | return LowestPrec 56 | } 57 | 58 | // IsScalar returns true if the token is a scalar 59 | func (tok Token) IsScalar() bool { 60 | switch tok { 61 | case INT: 62 | return true 63 | } 64 | return false 65 | } 66 | 67 | // IsUnary returns true if the token can be in a unary expression 68 | func (tok Token) IsUnary() bool { 69 | switch tok { 70 | case ADD, SUB: 71 | return true 72 | } 73 | return false 74 | } 75 | 76 | // IsLeftAssoc returns true if the token is left associative 77 | func (tok Token) IsLeftAssoc() bool { 78 | switch tok { 79 | case INT, ADD, SUB, MUL, QUO, REM: 80 | return true 81 | } 82 | return false 83 | } 84 | 85 | // IsRightAssoc returns true if the token is right associative 86 | func (tok Token) IsRightAssoc() bool { 87 | switch tok { 88 | case POW: 89 | return true 90 | } 91 | return false 92 | } 93 | 94 | // String provides a string representation of a Token 95 | func (tok Token) String() string { 96 | if 0 <= tok && tok < Token(len(tokens)) { 97 | return tokens[tok] 98 | } 99 | return tokens[0] 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pratt Top-down Operator Precedence Example in Go 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/richardjennings/pratt)](https://goreportcard.com/report/github.com/richardjennings/prattparser) [![codecov](https://codecov.io/gh/richardjennings/prattparser/branch/master/graph/badge.svg)](https://codecov.io/gh/richardjennings/prattparser) [![Build Status](https://travis-ci.org/richardjennings/prattparser.svg?branch=master)](https://travis-ci.org/richardjennings/prattparser) 3 | 4 | 5 | ## About 6 | 7 | The Pratt Top-down Operator Precedence algorithm addresses a difficulty or complexity inherent in handling operator precedence in a Recursive Decent Parser. 8 | The algorithm works by associating semantics alongside tokens instead of / as well as grammar rules that may be implemented in the parser. 9 | The algorithm enables the handling of: 10 | * infix and unary operators, e.g -10 (unary) 10 - 2 (infix) 11 | * right associativity, e.g. 1 ^ 2 ^ 3 => 1 ^ (2 ^ 3) not (1 ^ 2) ^ 3 12 | * operator precedence 13 | * parenthesis to override precedence 14 | 15 | 16 | ## How does it work? 17 | The scanner and token packages read a string such as "1 + 2 * 3" into a sequence of Tokens over which the parser can operate. 18 | E.g. Token INT with literal value 1, Token ADD, Token INT literal value 2 and so on. 19 | Each Token is associated with 2 behaviours and a weight. These are: 20 | * Null Denotation (nud) 21 | * Left Denotation (led) 22 | * Left Binding Power value (lbp) 23 | 24 | Null Denotation handles prefix contexts such as - 1. In this implementation, NUD creates either a scalar or unary ast node. 25 | Left Denotation handles infix contexts such as 2 - 1. In this implementation, LED creates a binary ast node. 26 | Left Binding Power is a weight which is used by the algorithm to determine the order by which operators in an expression should be evaluated. 27 | 28 | The core of the algorithm is: 29 | ``` 30 | func expr(rbp = 0) { 31 | left = NUD(next()) 32 | while rbp < LBP(peek()) { 33 | left = LED(left, next()) 34 | } 35 | return left 36 | } 37 | ``` 38 | The algorithm uses recursive calls to build a tree from the input stream of tokens. The right binding power is used to stop a 39 | recursive call from parsing past tokens with a lower binding power than the operator currently being handled. 40 | 41 | To illustrate; some examples: 42 | ``` 43 | //2 + 3 * 2 44 | expr(0) 45 | NUD(2) => ast.scalar(INT, '2') 46 | LBP(+) = 5 // greater than 0 47 | LED(left, next()) => LED(left, +) => ast.binary(ast.scalar(INT, '2'), +, expr(5)) 48 | ....expr(5) 49 | ....NUD(3) => ast.scalar(INT, '3') 50 | ....LBP(*) = 7 // greater than 5 51 | ....LED(left, next()) => LED(left, *) => ast.binary(ast.scalar(INT, '3'), *, expr(7)) 52 | ........expr(7) 53 | ........NUD(2) => ast.scalar(INT, '2') 54 | ........LBP() = 0 // not greater than 7 55 | ........return ast.scalar(INT, '2') 56 | ....return ast.binary(ast.scalar(INT, '3'), *, ast.scalar(INT, '2')) 57 | return ast.binary(ast.scalar(INT, '2'), +, ast.binary(ast.scalar(INT, '3'), *, ast.scalar(INT, '2'))) 58 | 59 | + 60 | / \ 61 | 2 * 62 | / \ 63 | 3 2 64 | 65 | 66 | 67 | // 2 * 2 + 1 68 | expr(0) 69 | NUD(2) => ast.scalar(INT, '2') 70 | LBP(*) = 7 // greater than 0 71 | LED(left, next()) => LED(left, *) => ast.binary(ast.scalar(INT, '2'), *, expr(7)) 72 | ....expr(7) 73 | ....NUD(2) => ast.scalar(INT, '2') 74 | ....LBP(+) = 5 // less than 7 75 | ....return ast.scalar(INT, '2') 76 | LBP(+) = 5 // more than 0 77 | LED(left, next()) => LED(left, '+') => ast.binary(ast.binary(ast.scalar(INT, '2'), *, ast.scalar(INT, '2')), +, expr(5)) 78 | ....expr(5) 79 | ....NUD(1) => ast.scalar(INT, '1') 80 | ....LBP() = 0 // less than 5 81 | ....return ast.scalar(INT, '1') 82 | return ast.binary(ast.binary(ast.scalar(INT, '2'), *, ast.scalar(INT, '2')), +, ast.scalar(INT, '1')) 83 | 84 | + 85 | / \ 86 | * 1 87 | / \ 88 | 2 2 89 | ``` 90 | 91 | Further reference articles: 92 | * [simple-top-down-parsing](http://effbot.org/zone/simple-top-down-parsing.htm) 93 | * [pratt-parsing](https://dev.to/jrop/pratt-parsing) 94 | 95 | 96 | ## Lexer Scanner 97 | 98 | The Scanner expects UTF8 input. The Lexer recognises integers and the following operators: 99 | 100 | | symbol | token | 101 | |---|---| 102 | | + | token.ADD | 103 | | - | token.SUB | 104 | | * | token.MUL | 105 | | / | token.QUO | 106 | | % | token.REM | 107 | | ^ | token.POW | 108 | | ( | token.LPAREN | 109 | | ) | token.RPAREN | 110 | 111 | 112 | ## Parser 113 | 114 | The Parser consumes tokens provided by the Lexer and applies the Pratt 115 | Operator Precedence Algorithm to generate an AST respecting operator precedence and associativity. 116 | 117 | ## Examples 118 | ``` 119 | $ go run cmd/parse/parse.go "1 + 2 * 3" 120 | 1 + (2 * 3) 121 | ``` 122 | ``` 123 | $ go run cmd/parse/parse.go "(1 + 2) * 3" 124 | (1 + 2) * 3 125 | ``` 126 | ``` 127 | $ go run cmd/parse/parse.go "2 ^ 3 ^ 4" 128 | 2 ^ (3 ^ 4) 129 | ``` 130 | 131 | ## Tests 132 | 133 | To run tests: 134 | ``` 135 | $ go test -v ./... 136 | ``` 137 | --------------------------------------------------------------------------------