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