Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Licence CC BY-NC-ND, Thierry Parmentelat

the pipeline

we’re building a tiny calculator that only knows about integers and the 4 operators + - * /, with parentheses for grouping.

three stages, three files:

stagefilesignatureinputoutput
lexlexer.pytokenize(file)a file-like objecta generator of tokens (strings)
parseparser.pyparse(tokens)a list of tokensan AST
evaleval.pyeval(ast)an ASTa number

an AST node is either

for example the expression 1 + 2 * 3 gets tokenized into

['1', '+', '2', '*', '3']

then parsed into

('+', 1, ('*', 2, 3))

and finally evaluates to 7

the lexer

and then you can check your code with

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[3], line 1
----> 1 assert tokenize_string("1 + 2 * 3") == ['1', '+', '2', '*', '3']

AssertionError: 
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[4], line 1
----> 1 assert tokenize_string("(1 + 2) * 30") == ['(', '1', '+', '2', ')', '*', '30']

AssertionError: 
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[5], line 2
      1 # whitespace, including newlines, is not significant
----> 2 assert tokenize_string("  12\n+\n  7 ") == ['12', '+', '7']

AssertionError: 

the parser

now that we can turn a string into a stream of tokens, we need to turn that stream into an AST.

we use a classic top-down (recursive-descent) parser, with one function per grammar rule; the grammar we implement is:

expr   := term (("+"|"-") term)*
term   := factor (("*"|"/") factor)*
factor := int | "(" expr ")"

And now it’s your turn:

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[8], line 1
----> 1 assert parse(tokenize_string("1 + 2 * 3")) == ('+', 1, ('*', 2, 3))

AssertionError: 
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[9], line 2
      1 # parentheses change the shape of the tree, not just the value
----> 2 assert parse(tokenize_string("(1 + 2) * 3")) == ('*', ('+', 1, 2), 3)

AssertionError: 
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[10], line 2
      1 # left-associativity: a - b - c is (a - b) - c, not a - (b - c)
----> 2 assert parse(tokenize_string("10 - 2 - 3")) == ('-', ('-', 10, 2), 3)

AssertionError: 

eval

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[12], line 1
----> 1 assert eval(parse(tokenize_string("1 + 2 * 3"))) == 7

AssertionError: 
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[13], line 1
----> 1 assert eval(parse(tokenize_string("(1 + 2) * 3"))) == 9

AssertionError: 
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[14], line 1
----> 1 assert eval(parse(tokenize_string("10 - 2 - 3"))) == 5

AssertionError: 
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[15], line 1
----> 1 assert eval(parse(tokenize_string("(30 + 40 + 50) * (20 - 15)"))) == 600

AssertionError: 

putting it all together

now that the 3 stages are in place, you can write a one-liner that goes straight from source to value:

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[17], line 1
----> 1 assert run("2 * (3 + 4) - 5") == 9

AssertionError: 

going further (optional)

a few ways you could extend this toy language, if you want to keep going: