Binding Power
Write an expression grammar the way you would say it, and the parse comes out
wrong: 2 * 3 + 5 groups by how the recursion happened to fall rather than by
what arithmetic means. Binding power is how you tell APM what the operators
are worth.
A rule bound to a table is parsed by a precedence loop instead of a greedy tail. Left recursion in the rule is fine — write it directly.
The table
Each operator gets two numbers: a left binding power and a right binding
power. Entries end with ;.
bindpow bp {
"or" : (10, 11) ;
"and" : (20, 21) ;
"==" : (30, 31) ;
"<" : (40, 41) ;
"+" : (50, 51) ;
"-" : (50, 51) ;
"*" : (60, 61) ;
"/" : (60, 61) ;
"not" : (0, 70) ;
};
Higher numbers bind tighter, so * takes its operands before + gets a look.
Associativity is the gap, not a keyword
There is no left or right to write. The relationship between an operator’s
two numbers is its associativity.
In APM, an operator grabs what is to its left when the left number is high enough to beat what is already holding it, and it keeps going to the right while the right number stays above what follows. So:
Written |
Means |
Because |
|---|---|---|
|
left-associative — |
the right side binds one step tighter, so the second |
|
right-associative — |
the right side binds one step looser, so the second |
|
left-associative, the same as |
a tie goes to whoever got there first |
The gap is always one step. Bigger gaps between the two numbers of the same operator buy nothing; it is the gap between different operators that sets precedence.
Prefix and postfix are a zero
A side that binds nothing is 0, and that is also how the kind is written:
|
prefix — |
|
postfix — |
|
infix — |
The slot is genuinely dead, so a 0 can never be mistaken for a real power. A
prefix arm is only ever matched where an operand is expected, where nothing
reads its left; a postfix arm folds immediately, so nothing reads its right.
What decides which kind an occurrence is is the grammar — where the alternative sits — not what the table calls it. The table only supplies the numbers.
Binding a rule to a table
feat {"bind": <table>} in front of the definition – feat always goes in
front of what it configures, an action or a semvar:
feat {"bind": bp} expr := atom
| expr . "or" . expr
| expr . "and" . expr
| expr . "==" . expr
| expr . "<" . expr
| expr . "+" . expr
| expr . "-" . expr
| expr . "*" . expr
| expr . "/" . expr
| "not" . expr
;
atom := number | string | ident | group;
group := "(" . expr . ")";
The first alternative that is not an operator arm is the operand — here
atom. The rest are the arms, and each must name an operator the table
declares.
A feat binding applies to the action itself, so every call site parses it
that way. It is the right choice when the rule is an expression rule and
there is no other way you would ever want it read.
The tree a bound rule makes
Without a map, a bound rule wraps each fold in a node of its own and the operator sits beside its operands:
expr "2 + 3 * 4"
├── atom "2"
├── text "+"
└── expr "3 * 4"
A map on the rule puts the operator on top, which is the shape every hand-written expression parser produces:
feat {"bind": bp} expr :=
( atom
| 'l'expr . "+" . 'r'expr
| 'l'expr . "*" . 'r'expr )
-> ( atom
| "+": ('l' 'r')
| "*": ('l' 'r') );
text "+"
├── atom "2"
└── text "*"
├── atom "3"
└── atom "4"
The two labels name the operands. They are not units of the arm – the loop supplies them – which is why they are the one place a label means something the body does not contain.
An arm may carry more than its operator
A ternary matches its middle operand inside the arm, and a subscript matches a whole expression between brackets. Both are ordinary units, and both keep what the map says about them:
bindpow post { "?" : (4, 3) ; "[" : (50, 0) ; "+" : (20, 21) ; };
feat {"bind": post} e :=
( num
| 'l'e . "+" . 'r'e
| 'c'e . "?" . 'q'e . ":" . 'x'e
| 'h'e . "[" . 'i'e . "]" )
-> ( num
| "+": ('l' 'r')
| "?": ('c' 'q' 'x')
| "[": ('h' 'i') );
1+2?3:4 1+2[3]
text "?" text "+"
├── text "+" ├── num "1"
├── e "3" └── text "["
└── num "4" ├── num "2"
└── e "3"
A unit the map does not name makes nothing, here as everywhere – which is what
drops the : and the ].
Note
['l'] on an operand is wrong, and quietly so. An operand is either a plain
unit or an earlier fold; ascend cannot tell them apart, so it would lift an
inner fold’s children out and flatten the nesting the powers just built.
Binding at the call site
The other way round is to leave the rule unbound and pick the table where you
call it, with table::-rule:
number = <0:9>+;
bindpow bp {
"+" : (50, 51) ;
"*" : (60, 61) ;
};
expr := number | expr . "+" . expr | expr . "*" . expr;
top := bp::-expr; # parse expr under bp, here
Over 2 + 3 * 4 that gives 2 + (3 * 4).
Use this when the same rule should be read with different precedences in different places, or when you want the grammar to stay readable as a plain recursive rule and the precedence to be a decision made elsewhere.
Important
Every operator the rule writes must appear in the table, and every operator in the table must appear in the rule. An operator that is not declared simply ends the expression, which would silently truncate a parse rather than fail it — so it is refused when the grammar is validated.
See also
bindpow and feat are reserved words, and a table’s name
shares the name space actions live in.