EBNF
A grammar for writing grammars: the notation half the specifications on your shelf are written in, read by a grammar of its own.
1(* An arithmetic expression, written with all three brackets *) 2 3<expression> ::= <term> { ("+" | "-") <term> } 4<term> ::= <factor> { ("*" | "/") <factor> } 5<factor> ::= [ "-" ] <primary> 6<primary> ::= <number> | <name> | "(" <expression> ")"
The interesting rule is Element, and the reason is the semicolon: EBNF ends
a rule with one, except when it does not. Without it, the name opening the
next rule looks exactly like a name used inside the current one, and a greedy
reading would swallow the whole file into the first rule.
The way out is a predicate. !(Identifier() ::T_ASSIGN::) matches nothing
and consumes nothing - it only asks whether what comes next is a name followed
by ::=, and fails if it is. A name that opens a rule therefore cannot be
read as part of the rule before it. See
Predicates for what else they are good for.
Grammar
1/** 2 * ----------------------------------------------------------------------------- 3 * EBNF 4 * ----------------------------------------------------------------------------- 5 * 6 * Backus-Naur form with the three brackets that make a repetition and an 7 * option writable without a rule of their own. 8 * 9 * @see https://www.iso.org/standard/26153.html 10 */ 11 12%pragma root RuleList 13 14%skip T_WHITESPACE \s++ 15%skip T_COMMENT \(\*.*?\*\) 16 17%token T_ASSIGN ::=|= 18%token T_ANGLE_OPEN < 19%token T_ANGLE_CLOSE > 20%token T_BRACKET_OPEN \[ 21%token T_BRACKET_CLOSE \] 22%token T_BRACE_OPEN \{ 23%token T_BRACE_CLOSE \} 24%token T_PARENTHESIS_OPEN \( 25%token T_PARENTHESIS_CLOSE \) 26%token T_OR \| 27%token T_SEMICOLON ; 28%token T_COMMA , 29 30%token T_STRING "[^"]*+"|'[^']*+' 31%token T_NAME [a-zA-Z][a-zA-Z0-9_-]*+ 32 33RuleList 34 : Rule()* 35 ; 36 37// <name> ::= alternatives 38Rule 39 : Identifier() ::T_ASSIGN:: Alternatives() ::T_SEMICOLON::? 40 ; 41 42Alternatives 43 : Alternative() (::T_OR:: Alternative())* 44 ; 45 46Alternative 47 : Element()* 48 ; 49 50/** 51 * A rule ends where the next one begins, and the semicolon closing it is 52 * optional, so a name followed by the separator opens a rule rather than 53 * belonging to the one before it. 54 */ 55Element 56 : Optional() 57 | Repetition() 58 | Group() 59 | Terminal() 60 | !(Identifier() ::T_ASSIGN::) Identifier() 61 ; 62 63// [ ... ] - written none or one time 64Optional 65 : ::T_BRACKET_OPEN:: Alternatives() ::T_BRACKET_CLOSE:: ::T_COMMA::? 66 ; 67 68// { ... } - written none or more times 69Repetition 70 : ::T_BRACE_OPEN:: Alternatives() ::T_BRACE_CLOSE:: ::T_COMMA::? 71 ; 72 73// ( ... ) - read as one 74Group 75 : ::T_PARENTHESIS_OPEN:: Alternatives() ::T_PARENTHESIS_CLOSE:: ::T_COMMA::? 76 ; 77 78// <name> or name 79Identifier 80 : ::T_ANGLE_OPEN:: <T_NAME> ::T_ANGLE_CLOSE:: ::T_COMMA::? 81 | <T_NAME> ::T_COMMA::? 82 ; 83 84Terminal 85 : <T_STRING> ::T_COMMA::? 86 ;
Usage
1use Phplrt\Compiler\Compiler; 2use Phplrt\Source\File; 3 4$parser = new Compiler() 5 ->load(new File(__DIR__ . '/grammar.pp3')) 6 ->getParser(); 7 8$rules = $parser->parse(new File(__DIR__ . '/expression.ebnf'));
Both spellings of everything are accepted on purpose: ::= and = for the
separator, <name> and name for a reference, and the commas ISO 14977
insists on between elements are read and dropped wherever they turn up.
25+ more grammars. phplrt/grammars collects ready to read grammars for real languages - JSON5, TSV, semantic versions, DQL, PHQL, JMS types, PSR-5 and Doctrine annotations, Symfony expressions, Go! AOP pointcuts, Praspel contracts and more - each with sample inputs and a test that keeps it honest.