Associativity
Precedence handles two different operators; associativity handles two of the same. `a - b - c` is `(a - b) - c` in every language you use, and `2 ** 3 ** 2` is 512 in Python and 64 in MATLAB — the same operator, associating opposite ways.
Why does a - b - c group to the left, and which operators do not?
An expression tree over a chain of equal-precedence operators. Associativity fixes which of the two possible spines that chain becomes: left-associative gives a tree that leans left, with the first operation deepest; right-associative leans right, with the last operation deepest. It exists to answer the one question precedence cannot: what happens when the competing operators are at the same level.
An associativity choice is a language decision, not an optimisation, so a parser must implement the one the specification names — for every operator, including the ones where it seems not to matter. It seems not to matter only for operators that are mathematically associative *and* exact; floating-point addition is neither, so even a + b + c has a specified grouping the compiler may not silently change. See [[optimization-legality]].
Key points
- Associativity decides grouping among operators at the same precedence level; precedence has nothing to say there.
- Left-associative puts the first operation deepest:
a - b - cis(a - b) - c. - Assignment as an expression is right-associative in C, C++, Java, C# and JavaScript; Python's multi-target assignment is a statement, not a right-associative operator.
- Exponentiation is right-associative in Python, Ruby, Fortran, Haskell, OCaml, R and JavaScript, and left-associative in MATLAB and Excel — 512 versus 64 for the same text.
- Non-associativity is a real third option: Rust rejects
a == b == coutright, and Python gives comparison chains a different meaning entirely. - Left and right recursive grammars accept identical inputs and build mirrored trees, so acceptance tests cannot detect the mistake.
- Floating-point addition is not associative, so even
a + b + chas a fixed grouping the optimizer may not change without permission.
The tree leans
Precedence answers "which of these two different operators is the parent". When the operators are the same, or are at the same precedence level, precedence has nothing to say and something else must decide. That something is associativity, and it is a per-level property of the language.
Left-associative means the leftmost operation is performed first and therefore sits deepest: a - b - c becomes (a - b) - c. Right-associative means the rightmost operation goes deepest: a = b = c becomes a = (b = c). Almost every arithmetic and comparison operator in almost every language is left-associative, which is why the topic feels like a non-issue until you meet one of the three families that are not.
Getting it wrong on + is invisible for integers and visible for floats. Getting it wrong on -, / or % is immediately, arithmetically wrong: 8 - 4 - 2 is 2 with the correct grouping and 6 with the other one.
a - b - c under left associativity — the standard readingRead it asThe spine goes down the left. The right-associative tree for the same tokens is the mirror image — root - with a on the left and -(b, c) on the right — and for 8 - 4 - 2 it evaluates to 6 instead of 2. Both trees are well-formed; only one matches the specification.
The operators that lean right
Three families are right-associative, and it is worth knowing why in each case rather than memorising a list.
Assignment, in the languages where it is an expression. a = b = c must mean a = (b = c) because the value being assigned to a is the result of the inner assignment; grouping it the other way would assign to the *result* of a = b, which is not a place. This holds in C, C++, Java, C# and JavaScript. Python is the interesting exception: x = y = 5 is not a right-associative expression at all but an assignment *statement* with two targets, evaluated left to right — a genuinely different construct that happens to look the same.
Exponentiation, where the convention follows mathematics: 2 ** 3 ** 2 is 2 ** (3 ** 2) = 2 ** 9 = 512. Python, Ruby, Fortran, Haskell (^, ^^ and **), OCaml, R and JavaScript's ** all agree. MATLAB and Excel do not: their ^ is left-associative, so 2^3^2 is (2^3)^2 = 64. Same notation, same precedence level, opposite association, factor-of-eight difference in the answer.
Conditional and arrow forms. C's ?: is right-associative, so a ? b : c ? d : e chains as a ? b : (c ? d : e) — which is what makes ternary chains readable as an if/else-if ladder. Function-type arrows in ML-family languages are right-associative for the same structural reason: a -> b -> c is a -> (b -> c), which is currying written down.
| Expression | Grouping | Value | Languages |
|---|---|---|---|
8 - 4 - 2spec | (8 - 4) - 2 | 2 | Everything with infix - |
2 ** 3 ** 2spec | 2 (3 2) | 512 | Python, Ruby, Fortran, JavaScript, R, OCaml |
2 ^ 3 ^ 2spec | (2 ^ 3) ^ 2 | 64 | MATLAB, Excel |
2 ^ 3 ^ 2spec | 2 ^ (3 ^ 2) | 512 | Haskell (^ is infixr 8) |
a = b = cspec | a = (b = c) | assignment as an expression | C, C++, Java, C#, JavaScript |
x = y = 5spec | two targets, one value | a statement, not an operator | Python |
a < b < cspec | (a < b) < c | compares a boolean to c | C, C++, Java, JavaScript |
a < b < cspec | a < b and b < c | b evaluated once | Python — chaining, not associativity |
a == b == cspec | rejected at compile time | no value | Rust — non-associative |
The third option: forbid it
An operator can also be declared *non-associative*, meaning a chain of it is a syntax error and the author must parenthesise. Bison spells this %nonassoc. Rust applies it to comparison operators: a == b == c does not compile, and the error message says comparison operators cannot be chained. Python takes the fourth route and gives chained comparisons a *different meaning* — a < b < c is a < b and b < c with b evaluated exactly once, which is neither left- nor right-associative but a separate grammatical construct.
Non-associativity is the right choice precisely when both groupings would be legal and one of them is almost certainly a mistake. a < b < c in C is legal, computes a comparison against a boolean, and is essentially never intended. Rust rejecting it is a diagnostic-quality decision paid for with a small loss of generality.
- Left-associative:
+ - * / %, and comparison and logical operators, in essentially every mainstream language. - Right-associative: assignment (as an expression), exponentiation in most languages,
?:in C-family languages,->in ML-family type syntax. - Non-associative: comparison in Rust; range operators in several languages; anything declared
%nonassocin a Bison grammar. - Something else entirely: Python's comparison chaining, which is a distinct construct with its own evaluation rule.
Implementing it, and the mistake everyone makes
In a layered grammar, associativity is which side you recurse on: expression → expression "-" term recurses on the left and yields left association; expression → term "-" expression recurses on the right and yields right association. The two grammars accept exactly the same inputs. This is the trap described in [[bnf-and-ebnf]], and it is the single most common way associativity is got wrong.
In a recursive-descent parser written from EBNF, the loop is the thing to look at. Build the node *inside* the loop with the accumulated result as the left operand, and you fold left. Collect the operands into a list and fold afterwards, and you have made an explicit choice that the grammar did not force — so write down which way you folded.
In a Pratt parser it is a single arithmetic detail: recurse with the operator's binding power for right associativity, and with the binding power plus one for left. That one increment is the entire implementation, which makes it very easy to get backwards and very hard to notice.
expression ::= expression "-" term | term
expression ::= term "-" expression | term
The rewrite preserves the accepted language exactly: both grammars generate term ("-" term)* for every length. It is legal as a *language* transformation, and a test suite that only checks which inputs parse will pass unchanged.
It is never legal as a *semantic* transformation for a non-associative operator. Under the second grammar 8 - 4 - 2 builds as 8 - (4 - 2) and evaluates to 6 rather than 2. For floating-point + it is equally illegal despite + being mathematically associative, because floating-point addition is not: (1e16 + 1.0) - 1e16 is 0.0 while 1e16 + (1.0 - 1e16) is 1.0, and a compiler may not reassociate them without an explicit fast-math option.
How it works
The steps, in the order the compiler takes them.
- Assign each precedence level an associativity: left, right, or none.
- In a layered grammar, recurse on the same side as the association — left-recursive rule for left association.
- In a recursive-descent loop, construct the binary node inside the loop with the accumulated node as the left child; that is a left fold.
- In a Pratt parser, recurse with binding power
bp + 1for left association andbpfor right, so an equal-power operator is or is not re-entered. - For non-associative operators, reject a second operator at the same level explicitly and emit a diagnostic naming the required parentheses.
- Test with a non-commutative operator and unequal operands —
8 - 4 - 2, never1 + 1 + 1— because commutative test data cannot distinguish the two trees.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A parser folds subtraction right, and
total - discount - taxproduces the wrong invoice total. Every test with symmetric data passes, and the bug reaches production. - An expression evaluator implements
**left-associatively; a formula that computes2 ** 3 ** 2returns 64 instead of 512, and a scientific result is quietly off by a factor of eight. - A C programmer writes
if (a < b < c)expecting a range check; it compiles, compares a 0-or-1 boolean againstc, and is true for nearly all values ofc— a validation check that never rejects anything. - A Pratt parser has the
bp + 1increment on the wrong operator family, so one precedence level associates backwards while the rest are correct — the bug is scoped to a handful of operators and looks like a data problem. - An optimizer reassociates a floating-point sum under an unnoticed fast-math flag, and a numerically sensitive reduction produces results that differ between build configurations.
When it helps
- Porting an expression evaluator or a formula language between hosts, where the exponentiation and comparison rules are exactly what differ.
- Reviewing a hand-written parser: checking which side each loop folds is a five-minute review that catches an entire bug class.
- Reading unfamiliar numerical code, where a stacked exponent or a chained comparison means something specific and language-dependent.
When it hurts
- Relying on associativity to make an expression readable.
a ? b : c ? d : eis well defined and still worth parenthesising, because the reader has to know the rule to read it. - Assuming mathematical associativity licenses reassociation. For floating point it does not, and for integer arithmetic in languages where overflow traps it does not either.
What it costs
Every one of these is paid by something.
- Making an operator right-associative buys the mathematical or structural convention users expect and costs a mirrored implementation path that the same tests cannot distinguish from the left one.
- Declaring an operator non-associative buys a diagnostic at the exact point of confusion and costs the ability to write a chain that some users legitimately wanted, plus a rule readers must learn.
- Python-style comparison chaining buys a genuinely useful range check and costs a special case in the grammar, in evaluation order and in every tool that walks the tree — the middle operand is evaluated once, which no ordinary binary node models.
What else you could do
What a different compiler or language does instead, and when that is better.
- Prefix or postfix notation removes the question: an s-expression or a Reverse Polish sequence states the tree directly, and no associativity rule is needed or possible.
- Requiring explicit parentheses for any chain of the same operator, which some policy and configuration languages do. It eliminates the rule and is verbose in precisely the arithmetic where the convention is universal anyway.
- Variadic nodes: rather than a chain of binary nodes, represent
a + b + cas one n-ary addition node and let a later phase decide the grouping. Some IRs do this deliberately so the optimizer can reassociate where the language permits — and it is the reason the language must say whether it permits it.
See it for yourself
The flag, dump or tool that shows you this directly.
python3 -c "print(2 ** 3 ** 2)"prints 512;python3 -c "import ast; print(ast.dump(ast.parse('2**3**2')))"shows the right-leaning tree that produced it.node -e "console.log(2 ** 3 ** 2)"prints 512, andnode -e "console.log(-2 ** 2)"is a SyntaxError — ES2016 deliberately requires the parentheses.clang -Xclang -ast-dump -fsyntax-only x.cona = b = cshows the nested assignment, with the inner one as the right child.rustcona == b == creports "comparison operators cannot be chained" and suggests the parentheses — the non-associative case made visible.bison -vprints how each%left,%rightand%nonassocdeclaration resolved each state, which is the fastest way to confirm a declaration did what you intended.
Plausible wrong readings
Stated the way a confident engineer states them.
- "Associativity only matters for
-and/." It matters for every operator that is not exactly associative, which includes floating-point+and*, and for every operator whose operands have side effects. - "Left-associative means evaluated left to right." It fixes the tree, not the evaluation order of operands within a node. In C the operands of one
+may be evaluated in either order regardless of associativity. - "Exponentiation is right-associative everywhere." MATLAB and Excel are left-associative, and both are used for numerical work where the difference is a wrong answer rather than a style question.
- "
x = y = 5proves Python's assignment is right-associative." It proves Python has multi-target assignment statements. Assignment is not an expression in Python at all, and the walrus operator:=was added precisely because it was not.
Misconceptions
The claim, and what is actually true.
+ is mathematically associative and still has a specified grouping, because the compiler is not allowed to change it for floating point.a < b < c desugars to a < b and b < c with b evaluated once, which no associativity rule can produce.Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
When two operators of the same precedence compete, associativity decides the grouping. Left-associative means the leftmost one happens first, which is what -, / and almost everything else does. Right-associative means the rightmost one happens first, which is what assignment and, in most languages, exponentiation do.
practical
Test associativity with asymmetric, non-commutative data. 8 - 4 - 2 distinguishes the two trees; 1 + 1 + 1 cannot. When porting formulas between environments, check exponentiation first: Python gives 512 for 2 ** 3 ** 2 and MATLAB gives 64 for 2^3^2, and that difference has produced real numerical bugs in ported models.
advanced
Associativity is also where language semantics constrain the optimizer. A left-associative floating-point sum is a specific sequence of roundings, so a compiler may not turn a serial reduction into a tree reduction — which is exactly the transformation that makes it vectorisable. That is why -ffast-math exists, why it is not the default, and why numerical libraries either accept the reassociation explicitly or use compensated summation to make the order matter less. Integer arithmetic is exactly associative in wrapping semantics, so the same reassociation is unconditionally legal there; in a language where signed overflow traps rather than wraps, it is not, because the reassociated version can trap on inputs the original does not. The associativity rule in the grammar and the legality of reassociation in the optimizer are the same fact seen from two ends of the pipeline.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
** is right-associative, the Haskell report fixes ^ at infixr 8, the MATLAB operator documentation states that power operators are left-associative, and the Rust reference lists comparison operators as requiring parentheses. These differ between languages and do not vary between implementations of one language.-ffast-math (GCC and Clang) or /fp:fast (MSVC) reassociation is explicitly permitted, and vectorised reductions then produce results that differ from the source order. Whether that is acceptable is a numerical decision, not a compiler one.If you were asked this in an interview
- Why is
a - b - cgrouped as(a - b) - c, and what would break if it were not? - Name two operators that are right-associative and say why each one has to be.
- A compiler wants to vectorise a floating-point sum. What does associativity have to do with whether it may?