The second argument of a reducer function changes based on the rule type. Getting this wrong is a common source of errors.
Reducer Argument Shapes
- Concatenation / Repetition: Receives a flat array of the children's reduced values. If you reference a token but don't use a reducer for it, its slot in the array will contain the raw
TokenInterface. - Alternation / Optional / Terminal: Receives a single value. For an absent optional, the value is an empty array
[] (not null). - Kept Terminal (
addTokenReference): If no reducer is provided, the reducer receives the raw TokenInterface. Access data via $token->value, $token->name, or $token->children.
Rule of thumb: Sequence $\rightarrow$ array, Choice/Terminal $\rightarrow$ scalar.
The Flattening Trap
If a rule (Rule B) is a Concatenation/Repetition and returns a bare array from its reducer, that array will be flattened into the parent rule's (Rule A) array. This makes it impossible for Rule A to distinguish where Rule B's children end.
To prevent flattening: Ensure the nested rule's reducer returns something that is not a bare array, such as an object or a scalar.
// BAD: $pair's array merges into $line's array
$pair = $builder->addConcatenation([$word, $word])
->setReducer(static fn($ctx, $kids) => $kids);
$line = $builder->addConcatenation([$pair, $word])
->setReducer(static fn($ctx, $kids) => $kids);
// $kids is ['a', 'b', 'c'], NOT [['a', 'b'], 'c']
// GOOD: $pair returns an object, so it survives intact
$pair = $builder->addConcatenation([$word, $word])
->setReducer(static fn($ctx, $kids) => (object) ['pair' => $kids]);