phplrt 4.0

Command Line

A grammar is edited far more often than the code around it, and two things get done over and over while editing: making sure the grammar still holds together, and writing the parser out again. Both are one command:

php vendor/bin/phplrt check resources/grammar.pp3
php vendor/bin/phplrt compile resources/grammar.pp3 src/Parser.php --class Parser

The binary ships with phplrt/compiler - and with phplrt/phplrt, which contains it - so composer puts it into vendor/bin on install. There is nothing to configure: it finds your autoloader on its own, and reads no config file of its own.

What Is In There

php vendor/bin/phplrt
1Usage:
2  command [options] [arguments]
3
4Available commands:
5  check       [validate] Check the passed grammar
6  compile     Compile the passed grammar
7  completion  Dump the shell completion script
8  help        Display help for a command
9  list        List commands

Two commands do the actual work. php vendor/bin/phplrt help compile describes either of them in full, and -V prints the version it runs as.

Checking A Grammar

check reads a grammar - and everything it %includes - then compiles it in memory and throws the result away. Nothing is written anywhere:

php vendor/bin/phplrt check resources/grammar.pp3
1Checking /app/resources/grammar.pp3 grammar
2Loaded 1 grammar files:
3  - /app/resources/grammar.pp3
4
5[OK] grammar is valid

validate is the same command under a different name, for whichever of the two your fingers prefer.

The listed files are the ones that declared a rule, so a grammar split up with %include shows where its rules came from:

1Checking /app/resources/grammar.pp3 grammar
2Loaded 3 grammar files:
3  - /app/resources/grammar/literals.pp3
4  - /app/resources/grammar/expressions.pp3
5  - /app/resources/grammar.pp3

A file that only declares tokens is read all the same, but has no rules to show up with.

The exit code is 0 for a grammar that compiles and 1 for one that does not, which is the reason the command exists - a broken grammar should fail in a pull request:

- run: php vendor/bin/phplrt check resources/grammar.pp3

Anything the compiler will not accept is named:

1Checking /app/resources/grammar.pp3 grammar
2
3In RuleReferenceResolutionParserCompilerPass.php line 104:
4
5  Rule Sum = Number refers to the rule named "Number", which has not been defined
1Checking /app/resources/grammar.pp3 grammar
2
3In UnexpectedTokenException.php line 21:
4
5  Syntax error, unexpected ";" (T_SEMICOLON), T_ANGLE_CLOSE expected

Add -v for the exception class and the stack trace behind it.

The Numbers Behind The Answer

"Valid" is a low bar - a grammar can compile and still not be the grammar you meant. -v prints what the compiler made of it:

php vendor/bin/phplrt check resources/grammar.pp3 -v
 1[OK] grammar is valid
 2
 3 Reducers:  2
 4 Lookahead: 4 / 1
 5 Kept:      2
 6 Channels:  2
 7 Subgroups: 0
 8 PCRE:      100 bytes
 9
10 Rules:
11   Loaded: 7
12   After:  5
13
14 Tokens:
15   Loaded: 3
16   After:  4
Line What it counts
Reducers Rules that build a result - the ones written with -> { ... }
Lookahead Entries in the lookahead table / rules it cannot cover (more / less)
Kept Rules that need tracing at runtime (less is better)
Channels Tokens sitting on a channel other than the default one
Subgroups Tokens whose pattern captures a subgroup (less is better)
PCRE Size of the single regular expression the lexer became
Rules Rule nodes built from the grammar, and what is left once folded away
Tokens Tokens declared, and tokens compiled

Two of those lines are worth reading every time.

Tokens grows by one. Three tokens were declared and four came out: the compiler appends a catch-all so that unrecognized input becomes an Unknown token instead of an error. See Code Generation.

Rules shrinks - and how much it shrinks matters. Folding transition rules away is normal. Losing almost everything is not:

1 Reducers:  1
2
3 Rules:
4   Loaded: 7
5   After:  1

Seven rules in, one rule out, one reducer left of two. The grammar is valid; it is just wired to the wrong root. Parsing starts at the first rule declared unless %pragma root says otherwise, and an %include that declares rules of its own is easy to leave sitting above the rule you meant to start from - everything unreachable from the real root is then dropped. Naming the root brings them back:

%pragma root Sum
1 Reducers:  2
2
3 Rules:
4   Loaded: 8
5   After:  5

-vv prints the same numbers with a line of explanation above each, if you would rather not keep the table above at hand:

1 // Lookahead table / non-optimizable rules size (more / less is better)
2 Lookahead: 4 / 1
3
4 // List of rules that requires tracing (less is better)
5 Kept:      2

Compiling A Grammar

compile writes the parser down as PHP:

php vendor/bin/phplrt compile resources/grammar.pp3 src/Parser.php
1Loading /app/resources/grammar.pp3 grammar
2 [OK] Generated into src/Parser.php

Both arguments are required: the grammar to read and the file to write. Any directory missing on the way is created.

Option Meaning
-c, --class The class name to declare
--namespace The namespace to declare it in
-u, --use A class to import; repeat it for more than one
php vendor/bin/phplrt compile resources/grammar.pp3 src/Parser/SumParser.php \
    --class SumParser \
    --namespace "App\Parser" \
    --use "App\Ast\Node" \
    --use "App\Ast\Number"
1namespace App\Parser;
2
3use App\Ast\Node;
4use App\Ast\Number;
5
6readonly class SumParser extends \Phplrt\Parser\Parser { /* ... */ }
$parser = new App\Parser\SumParser();

Imports are what makes the PHP inside a grammar readable - new Node(...) instead of new \App\Ast\Node(...) - at the price of a grammar that only works when it is generated. See PHP in a Grammar.

Leave --class out and the file returns an anonymous parser instead of declaring anything:

php vendor/bin/phplrt compile resources/grammar.pp3 build/parser.php
return new readonly class extends \Phplrt\Parser\Parser { /* ... */ };
$parser = require __DIR__ . '/../build/parser.php';

Use the named form for anything autoloaded, the anonymous one for a build artifact you do not want in the class namespace at all. Code Generation covers what comes out of either.

Running It From A Script

The two commands end up in composer.json and in CI far more often than they are typed by hand - Automation and CI has the composer scripts, the GitHub Actions and GitLab CI jobs, and what to watch out for when there is more than one grammar.

Shell Completion

php vendor/bin/phplrt completion bash > /etc/bash_completion.d/phplrt

bash, zsh and fish are supported - the shell is guessed from $SHELL when the argument is left out. Command and option names complete from there on.

When The CLI Is Not Enough

The two commands cover the shape almost every project needs. Everything past that is a few lines of PHP:

  • generating into a format that is not PHP - a token list for a highlighter, say - needs a generator of your own;
  • importing a class under an alias (use App\Ast\Node as BaseNode);
  • loading several grammars into one parser;
  • getting the code as a string without writing it, to diff or to reformat.

All of it is in Code Generation, and the compiler itself is described in Compiling a Grammar.