phplrt 4.0

Custom Generator

A PHP parser is not the only thing a compiled grammar is good for. An editor plugin wants the token list; a syntax highlighter wants the pattern; a client in another language wants the rules. All of that is in CompilerResult, and the interface for turning it into a file has one method:

1public function generate(
2    CompilerResult $result,
3    OutputContext $context = new OutputContext(),
4): string;

Return a string, and everything else - withNamespaceName(), save(), creating directories - keeps working as before.

Here is a generator that writes the token list down as JSON, for a highlighter that needs to know the words of a language but not its grammar:

 1use Phplrt\Compiler\CompilerResult;
 2use Phplrt\Compiler\Generator\OutputContext;
 3use Phplrt\Compiler\Generator\OutputGeneratorInterface;
 4
 5final readonly class TokenListGenerator implements OutputGeneratorInterface
 6{
 7    public function generate(
 8        CompilerResult $result,
 9        OutputContext $context = new OutputContext(),
10    ): string {
11        $tokens = [];
12
13        foreach ($result->lexer->tokens as $id => $definition) {
14            $tokens[] = [
15                'id' => $id,
16                'name' => $result->lexer->names[$id] ?? null,
17                'channel' => $result->lexer->channels[$id] ?? 'Default',
18            ];
19        }
20
21        return \json_encode([
22            'name' => $context->class ?? 'grammar',
23            'pattern' => $result->lexer->pattern,
24            'tokens' => $tokens,
25        ], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) . "\n";
26    }
27}

Pass it to generate() in place of the default:

1new Compiler()
2    ->load(FileSource::createFromPathname(__DIR__ . '/grammar.pp3'))
3    ->generate(new TokenListGenerator())
4        ->withClassName('Calculator')
5        ->save(__DIR__ . '/calculator.json');

Given this grammar:

1%skip  T_WHITESPACE  \s++
2%skip  T_COMMENT     //[^\n]*+
3
4%token T_NUMBER      \d++
5%token T_PLUS        \+
6
7Sum -> { return \array_sum($children); }
8  : <T_NUMBER> (::T_PLUS:: <T_NUMBER>)*
9  ;

you get calculator.json:

 1{
 2    "name": "Calculator",
 3    "pattern": "/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:\\/\\/[^\\n]*+)(*MARK:1))|(?:(?:\\d++)(*MARK:2))|(?:(?:\\+)(*MARK:3))|(?:(?:[^\\s]++)(*MARK:4)))/Ssum",
 4    "tokens": [
 5        {
 6            "id": 0,
 7            "name": "T_WHITESPACE",
 8            "channel": "Hidden"
 9        },
10        {
11            "id": 1,
12            "name": "T_COMMENT",
13            "channel": "Hidden"
14        },
15        {
16            "id": 2,
17            "name": "T_NUMBER",
18            "channel": "Default"
19        },
20        {
21            "id": 3,
22            "name": "T_PLUS",
23            "channel": "Default"
24        },
25        {
26            "id": 4,
27            "name": null,
28            "channel": "Unknown"
29        }
30    ]
31}

Two things the output shows that the grammar does not say out loud: the %skip tokens are ordinary tokens on the Hidden channel, and token #4 is the catch-all the compiler appends so that unrecognized input becomes an Unknown token instead of an error.

What You Get To Work With

 1$result->lexer->tokens;      // array<int, TokenDefinition>
 2$result->lexer->names;       // array<int, non-empty-string>
 3$result->lexer->channels;    // array<int, non-empty-string>
 4$result->lexer->pattern;     // the compiled regex
 5$result->lexer->transitions; // which tokens enter a nested lexer
 6$result->lexer->lexers;      // the nested lexers themselves
 7
 8$result->parser->grammar;    // list<RuleInterface> - the rules, by id
 9$result->parser->initial;    // where parsing starts
10$result->parser->reducers;   // array<int, ReducerInterface>
11$result->parser->constants;  // ['Sum' => 0, ...] - named rules
12$result->parser->lookahead;
13$result->parser->kept;

OutputContext carries what the caller asked for - $context->namespace, $context->class, $context->imports. Use what makes sense for your format and ignore the rest.

Reporting Problems

Not every grammar can be written into every format. Throw a GeneratorException when yours cannot express something, so the failure arrives as a compiler error rather than as broken output:

1use Phplrt\Compiler\Exception\GeneratorException;
2
3if ($result->lexer->lexers !== []) {
4    throw new class ('Nested lexers cannot be written as JSON')
5        extends GeneratorException {};
6}

Adjusting The PHP Output Instead

If you only want to change how the PHP looks, you do not need a generator of your own. PhpOutputGenerator renders Twig templates from the compiler's resources/php/ directory, and they are split per fragment - the file layout, the parser body, the grammar table, the reducer methods, the lexer and its states - so a change is usually confined to one small template.