phplrt 4.0

Grammar Rules

A grammar is a flat array of rules. Each rule sits at an index, and rules refer to each other by that index:

1$grammar = [
2    0 => new Concatenation([1, 2]), // "read rule #1, then rule #2"
3    1 => new Lexeme(tokenId: 0),
4    2 => new Lexeme(tokenId: 1),
5];

There are five rule classes, and between them they cover everything EBNF can say. You will usually get them from a grammar file or the builder, but it helps to know what each one does.

Lexeme

Reads a single token. This is where a grammar touches the actual input - every other rule is written in terms of other rules.

1use Phplrt\Parser\Grammar\Lexeme;
2
3new Lexeme(tokenId: 0);              // read T_DIGIT, keep it
4new Lexeme(tokenId: 1, keep: false); // read T_COMMA, throw it away
1Rule : <T_DIGIT> ;    // keep
2Rule : ::T_COMMA:: ;  // read and discard

keep: false is for punctuation. A comma between list items has to be there, but nobody needs it in the result - dropping it early means you do not have to filter it out later.

Note the token is addressed by id, not by name. What it looks like in the source is the lexer's business.

Concatenation

Reads several rules, one after another. All of them must match, in order.

1use Phplrt\Parser\Grammar\Concatenation;
2
3new Concatenation([1, 2, 1]);
Rule : Number() Plus() Number() ;

If any of them fails, the whole sequence fails: the input rewinds to where the sequence started and everything read along the way is dropped.

Alternation

Tries the rules in order and takes the first one that matches.

1use Phplrt\Parser\Grammar\Alternation;
2
3new Alternation([1, 2]);
Rule : Number() | Name() ;

The order is part of the meaning. The first match wins, and the rest are never tried - even if one of them would have read more of the input:

1Rule : "a" | "ab" ;   // never reads "ab"
2Rule : "ab" | "a" ;   // ✔

This is what makes a PEG grammar unambiguous. If you are coming from a classic EBNF tool that picks the longest match, this is the one habit you need to unlearn.

Optional

Reads a rule if it is there, and succeeds either way.

1use Phplrt\Parser\Grammar\Optional;
2
3new Optional(ruleId: 1);
Rule : Sign()? ;

If the inner rule does not match, nothing is read and nothing is added to the result - the parse simply continues from the same place.

Repetition

Reads a rule as many times as it keeps matching.

1use Phplrt\Parser\Grammar\Repetition;
2
3new Repetition(ruleId: 1);                 // zero or more, "*"
4new Repetition(ruleId: 1, min: 1);         // one or more,  "+"
5new Repetition(ruleId: 1, min: 2, max: 5); // between two and five
1Rule : Number()* ;
2Rule : Number()+ ;
3Rule : Number(){2,5} ;

Repetition is greedy: it reads as many times as it can and does not give any of them back. It also stops as soon as an iteration reads nothing, so a rule that matches the empty input cannot loop forever.

Predicate

Looks at what comes next without reading it. Nothing is consumed and nothing lands in the result - the only thing left is whether it matched.

1use Phplrt\Parser\Grammar\Predicate;
2
3new Predicate(ruleId: 1);                    // "&" - must match here
4new Predicate(ruleId: 1, isExpected: false); // "!" - must not match here

This is how a rule refuses a position that belongs to somebody else. For example, "a name that is not a function call":

1new Concatenation([
2    2, // Predicate(ruleId: 1 /* "(" */, isExpected: false)
3    3, // Lexeme(T_NAME)
4]);

EBNF has nothing like this - a predicate describes how something is read rather than what the language contains. A grammar file writes it with the same two signs:

1Variable : <T_NAME> !::T_PARENTHESIS_OPEN:: ;
2Closure  : &::T_FN:: FunctionLiteral() ;

See Predicates.

Sequences and Single Values

One distinction matters when you write reducers.

Concatenation and Repetition implement SequenceInterface: they recognize a list of things, so their value is a list.

1Rule : Number() Plus() Number() ;  // $children is an array
2Rule : Number()+ ;                 // $children is an array

Everything else passes a single value through:

1Rule : Number() | Name() ;   // $children is whatever matched
2Rule : Number()? ;           // $children is the Number, or nothing
3Rule : <T_DIGIT> ;           // $children is the token

That is why a reducer often starts with an is_array() check - a rule that can match one thing or several will hand you one thing or several.

More on this in Results and Reducers.

The Interfaces

1RuleInterface
2├── TerminalInterface     - Lexeme
3└── ProductionInterface   - Alternation, Optional, Predicate
4    └── SequenceInterface - Concatenation, Repetition

A terminal is matched against the input. A production is matched by means of other rules. A sequence is a production whose value is a list.