packages/lemon-parser/src/Parser.php

1<?php
2
3declare(strict_types=1);
4
5namespace LemonParser;
6
7use LemonParser\Ast\GrammarFile;
8use LemonParser\Preprocessor\Preprocessor;
9use LemonParser\Scanner\Scanner;
10use LemonParser\Syntax\GrammarReader;
11use LemonParser\Syntax\TokenStream;
12
13/**
14 * Reads a Lemon grammar file into a tree that keeps everything the file says.
15 *
16 * The file is read as Lemon reads it: `%ifdef` regions are settled with the
17 * names given, the text is split into tokens the way `lemon.c` splits it,
18 * and rules and declarations are accepted and rejected where Lemon accepts
19 * and rejects them.
20 *
21 * @visibility public
22 *
23 * @example Reading a grammar
24 *     $file = (new \LemonParser\Parser())->parse("%token_prefix TK_\n%left PLUS MINUS.\nexpr(A) ::= expr(B) PLUS expr(C). { A = B + C; }\nexpr(A) ::= NUM(B). { A = B; }\n");
25 *     [count($file->declarations()), count($file->rules()), $file->rules()[1]->items[0]->symbols[0]->name] // => [2, 2, 'NUM']
26 * @example Settling a conditional region
27 *     $source = "%ifndef OMIT_WINDOW\nwindow ::= OVER.\n%endif\ncmd ::= SELECT.\n";
28 *     $parser = new \LemonParser\Parser();
29 *     [count($parser->parse($source)->rules()), count($parser->parse($source, ['OMIT_WINDOW'])->rules())] // => [2, 1]
30 * @example Rejecting what Lemon rejects
31 *     try { (new \LemonParser\Parser())->parse("expr ::= expr ? expr.\n"); } catch (\LemonParser\SyntaxException $e) { $error = $e->getMessage(); }
32 *     $error // => 'Illegal character on RHS of rule: "?". at 1:15'
33 */
34final class Parser
35{
36    /**
37     * @param Preprocessor $preprocessor Settles conditional regions
38     * @param Scanner $scanner Splits the text into tokens
39     * @param GrammarReader $reader Builds the tree
40     */
41    public function __construct(
42        private readonly Preprocessor $preprocessor = new Preprocessor(),
43        private readonly Scanner $scanner = new Scanner(),
44        private readonly GrammarReader $reader = new GrammarReader(),
45    ) {
46    }
47
48    /**
49     * Reads a grammar file.
50     *
51     * @param string $source The file's text
52     * @param list<string> $defines Names defined for `%ifdef`, as Lemon's `-D` option defines them
53     *
54     * @return GrammarFile The tree
55     *
56     * @throws SyntaxException Where Lemon reports an error
57     */
58    public function parse(string $source, array $defines = []): GrammarFile
59    {
60        $tokens = $this->scanner->scan($this->preprocessor->preprocess($source, $defines));
61
62        return $this->reader->read(new TokenStream($tokens));
63    }
64}
65