packages/lemon-parser/src/Syntax/SymbolListReader.php

1<?php
2
3declare(strict_types=1);
4
5namespace LemonParser\Syntax;
6
7use LemonParser\Ast\Symbol;
8use LemonParser\Scanner\Token;
9use LemonParser\Scanner\TokenKind;
10use LemonParser\SyntaxException;
11
12/**
13 * Reads the lists of terminals that end with a period.
14 *
15 * @visibility root
16 */
17final class SymbolListReader
18{
19    /**
20     * Reads the terminals of `%left`, `%right` or `%nonassoc`.
21     *
22     * @param TokenStream $tokens The rest
23     * @param SymbolRegistry $registry Symbols seen so far
24     *
25     * @return list<Symbol> The terminals
26     *
27     * @throws SyntaxException When something is not a terminal or already ranked
28     */
29    public function ranked(TokenStream $tokens, SymbolRegistry $registry): array
30    {
31        $symbols = [];
32        foreach ($this->untilPeriod($tokens) as $token) {
33            if (!$token->isUpperWord()) {
34                throw new SyntaxException("Can't assign a precedence to \"{$token->raw}\".", $token->location);
35            }
36            $registry->rank($token->text, $token->location);
37            $symbols[] = new Symbol($token->text, $token->location);
38        }
39
40        return $symbols;
41    }
42
43    /**
44     * Reads the terminals of `%fallback`.
45     *
46     * @param TokenStream $tokens The rest
47     * @param SymbolRegistry $registry Symbols seen so far
48     *
49     * @return list<Symbol> The fallback first, then the tokens that fall back to it
50     *
51     * @throws SyntaxException When something is not a terminal or already falls back
52     */
53    public function fallbacks(TokenStream $tokens, SymbolRegistry $registry): array
54    {
55        $symbols = [];
56        foreach ($this->untilPeriod($tokens) as $token) {
57            if (!$token->isUpperWord()) {
58                throw new SyntaxException("%fallback argument \"{$token->raw}\" should be a token", $token->location);
59            }
60            if ($symbols === []) {
61                $registry->see($token->text);
62            } else {
63                $registry->fallBack($token->text, $token->location);
64            }
65            $symbols[] = new Symbol($token->text, $token->location);
66        }
67
68        return $symbols;
69    }
70
71    /**
72     * Reads the terminals of `%token`.
73     *
74     * @param TokenStream $tokens The rest
75     * @param SymbolRegistry $registry Symbols seen so far
76     * @param string $keyword The keyword, for messages
77     *
78     * @return list<Symbol> The terminals
79     *
80     * @throws SyntaxException When something is not a terminal
81     */
82    public function tokens(TokenStream $tokens, SymbolRegistry $registry, string $keyword): array
83    {
84        $symbols = [];
85        foreach ($this->untilPeriod($tokens) as $token) {
86            if (!$token->isUpperWord()) {
87                throw new SyntaxException("%{$keyword} argument \"{$token->raw}\" should be a token", $token->location);
88            }
89            $registry->see($token->text);
90            $symbols[] = new Symbol($token->text, $token->location);
91        }
92
93        return $symbols;
94    }
95
96    /**
97     * Reads the terminal of `%wildcard`.
98     *
99     * @param TokenStream $tokens The rest
100     * @param SymbolRegistry $registry Symbols seen so far
101     *
102     * @return Symbol|null The terminal, or null when none was written
103     *
104     * @throws SyntaxException When something is not a terminal or a second one is named
105     */
106    public function wildcard(TokenStream $tokens, SymbolRegistry $registry): ?Symbol
107    {
108        $symbol = null;
109        foreach ($this->untilPeriod($tokens) as $token) {
110            if (!$token->isUpperWord()) {
111                throw new SyntaxException("%wildcard argument \"{$token->raw}\" should be a token", $token->location);
112            }
113            $registry->wildcard($token->text, $token->location);
114            $symbol = new Symbol($token->text, $token->location);
115        }
116
117        return $symbol;
118    }
119
120    /**
121     * Reads the terminals of `%token_class`, written bare or after `|` or `/`.
122     *
123     * @param TokenStream $tokens The rest
124     * @param SymbolRegistry $registry Symbols seen so far
125     *
126     * @return list<Symbol> The terminals
127     *
128     * @throws SyntaxException When something is not a terminal
129     */
130    public function classTokens(TokenStream $tokens, SymbolRegistry $registry): array
131    {
132        $symbols = [];
133        foreach ($this->untilPeriod($tokens) as $token) {
134            $compound = $token->is(TokenKind::Compound) && ctype_upper($token->text[0]);
135            if (!$token->isUpperWord() && !$compound) {
136                throw new SyntaxException("%token_class argument \"{$token->raw}\" should be a token", $token->location);
137            }
138            $registry->see($token->text);
139            $symbols[] = new Symbol($token->text, $token->location);
140        }
141
142        return $symbols;
143    }
144
145    /**
146     * Takes tokens up to the period that ends a list.
147     *
148     * @param TokenStream $tokens The rest
149     *
150     * @return list<Token> The tokens before the period
151     *
152     * @throws SyntaxException When the file ends first
153     */
154    public function untilPeriod(TokenStream $tokens): array
155    {
156        $taken = [];
157        while (true) {
158            $token = $tokens->next();
159            if ($token->isPunctuation('.')) {
160                return $taken;
161            }
162            if ($token->is(TokenKind::End)) {
163                throw new SyntaxException('Declaration is not terminated by "." before the end of the file.', $token->location);
164            }
165            $taken[] = $token;
166        }
167    }
168}
169