Abstract syntax tree precedence example (source code)

= Abstract syntax tree precedence example

Consider <Python> programs `x=2+3*4` and `x=(2+3)*4`. <Operator precedence> places <multiplication> inside the right operand of <addition> in the first <abstract syntax tree>. The parentheses instead place <addition> inside the left operand of <multiplication> in the second. Their right-hand sides evaluate to $2+(3\cdot4)=14$ and $(2+3)\cdot4=20$. No rule that simply reads all operators left to right would recover both structures. https://docs.python.org/3.14/reference/expressions.html#operator-precedence[Python language reference, section 6.17, multiplicative operators bind more tightly than additive operators]{ref}

\Image[/abstract-syntax-tree-precedence.png]
{title=Two <abstract syntax trees> for <variable assignment>: <operator precedence> gives `x=2+3*4` the value $14$, while parentheses give `x=(2+3)*4` the value $20$}
{id=image-abstract-syntax-tree-precedence}

These are schematic views of <Python> trees: `Module` is omitted, and the `Add`/`Mult` and `Store` markers are written inside their owning nodes. In the actual schema, `BinOp` has `left`, `op` and `right` fields; `Assign` has a list of `targets` and a `value`. `Name(id='x', ctx=Store())` marks the name as a <variable assignment> target. Reading `x` in a <program expression> instead uses `Load`. A <variable assignment> is a statement, not an <equation> asserting that two values are equal. <Abstract syntax schema for CPython 3.14>[CPython 3.14, Parser/Python.asdl: Module, Assign, BinOp, Constant, Name and expr_context constructors]{ref}

A restricted recursive evaluator can follow those fields. It accepts only <integer> <literal values> and binary <addition>, <subtraction> and <multiplication>; the surrounding <variable assignment> remains a separate action. Using `type(value) is int` excludes Boolean constants, even though <Python> treats `bool` as a subclass of `int`.

``
import ast

OPS = {ast.Add: lambda a, b: a + b,
       ast.Sub: lambda a, b: a - b,
       ast.Mult: lambda a, b: a * b}

def evaluate(node):
    if isinstance(node, ast.Constant) and type(node.value) is int:
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in OPS:
        return OPS[type(node.op)](evaluate(node.left), evaluate(node.right))
    raise ValueError("outside the integer-expression language")

for source in ("x=2+3*4", "x=(2+3)*4"):
    tree = ast.parse(source, optimize=0)
    statement = tree.body[0]
    print(statement.targets[0].id, evaluate(statement.value))
# x 14
# x 20
``

The <children in a rooted tree> have ordered roles: `left` and `right` cannot be interchanged. For <subtraction>, `8-3` gives $5$ while `3-8` gives $-5$. <Operator associativity> also matters: `8-3-1` parses as `(8-3)-1`, giving $4$, whereas `8-(3-1)` gives $6$. Here the syntax groups operators to the left; this does not assert the mathematical <associativity> of <subtraction>. <Operator precedence> determines grouping, rather than a general permission to reorder execution. For example, <Python> evaluates the calls in `f()+g()*h()` in the order `f`, `g`, `h`, although the <multiplication> result must be ready before the outer <addition>. https://docs.python.org/3.14/reference/expressions.html#evaluation-order[Python language reference, sections 6.16 and 6.17: left-to-right evaluation and left grouping of additive operators]{ref}