STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
Lexer.cpp
Go to the documentation of this file.
1#include "Lexer.hpp"
2
3#include <cctype>
4#include <cstdlib>
5
6namespace fsw {
7namespace script {
8
9const char* tok_name(Tok t) {
10 switch (t) {
11 case Tok::Eof:
12 return "end of script";
13 case Tok::Newline:
14 return "end of line";
15 case Tok::Indent:
16 return "indent";
17 case Tok::Dedent:
18 return "dedent";
19 case Tok::Ident:
20 return "a name";
21 case Tok::Number:
22 return "a number";
23 case Tok::Colon:
24 return "':'";
25 case Tok::LParen:
26 return "'('";
27 case Tok::RParen:
28 return "')'";
29 case Tok::Comma:
30 return "','";
31 case Tok::Assign:
32 return "'='";
33 case Tok::Plus:
34 return "'+'";
35 case Tok::Minus:
36 return "'-'";
37 case Tok::Star:
38 return "'*'";
39 case Tok::Slash:
40 return "'/'";
41 case Tok::Lt:
42 return "'<'";
43 case Tok::Le:
44 return "'<='";
45 case Tok::Gt:
46 return "'>'";
47 case Tok::Ge:
48 return "'>='";
49 case Tok::EqEq:
50 return "'=='";
51 case Tok::Ne:
52 return "'!='";
53 case Tok::KwIf:
54 return "'if'";
55 case Tok::KwElif:
56 return "'elif'";
57 case Tok::KwElse:
58 return "'else'";
59 case Tok::KwWhile:
60 return "'while'";
61 case Tok::KwAnd:
62 return "'and'";
63 case Tok::KwOr:
64 return "'or'";
65 case Tok::KwNot:
66 return "'not'";
67 }
68 return "?";
69}
70
71namespace {
72
73bool identStart(char c) {
74 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
75}
76bool identChar(char c) {
77 return identStart(c) || (c >= '0' && c <= '9');
78}
79
80Tok keywordOf(const std::string& s) {
81 if (s == "if")
82 return Tok::KwIf;
83 if (s == "elif")
84 return Tok::KwElif;
85 if (s == "else")
86 return Tok::KwElse;
87 if (s == "while")
88 return Tok::KwWhile;
89 if (s == "and")
90 return Tok::KwAnd;
91 if (s == "or")
92 return Tok::KwOr;
93 if (s == "not")
94 return Tok::KwNot;
95 return Tok::Ident;
96}
97
98struct Lexer {
99 const std::string& src;
101 size_t i = 0;
102 uint32_t line = 1;
103 uint32_t col = 1;
104 /** Column stack for open blocks; [0] is always 0. */
105 std::vector<uint32_t> indents{0};
106 /** Set by the first indented line; every later indent must be a multiple of it. 0 = unset. */
107 uint32_t unit = 0;
108 bool failed = false;
109
110 explicit Lexer(const std::string& s) : src(s) {
111 }
112
113 void fail(Diag code, uint32_t l, uint32_t c, std::string msg) {
114 if (failed)
115 return;
116 failed = true;
117 out.diagnostics.push_back({code, l, c, std::move(msg)});
118 }
119
120 void push(Tok k, uint32_t l, uint32_t c, uint32_t len, std::string text = {},
121 double num = 0.0) {
122 Token t;
123 t.kind = k;
124 t.line = l;
125 t.col = c;
126 t.len = len;
127 t.text = std::move(text);
128 t.number = num;
129 out.tokens.push_back(std::move(t));
130 }
131
132 char peek(size_t off = 0) const {
133 return (i + off < src.size()) ? src[i + off] : '\0';
134 }
135
136 void run() {
137 bool at_line_start = true;
138 while (!failed && i < src.size()) {
139 if (at_line_start) {
140 if (!handleLineStart(at_line_start))
141 return;
142 continue;
143 }
144 char c = peek();
145 if (c == '\n') {
147 i++;
148 line++;
149 col = 1;
150 at_line_start = true;
151 continue;
152 }
153 if (c == '\r') { // CRLF normalised at the door
154 i++;
155 continue;
156 }
157 if (c == ' ') {
158 i++;
159 col++;
160 continue;
161 }
162 if (c == '#') {
163 while (i < src.size() && src[i] != '\n')
164 i++;
165 continue;
166 }
167 if (!lexToken())
168 return;
169 }
170 if (failed)
171 return;
172 // A file that does not end in a newline still closes its last statement.
173 if (!out.tokens.empty() && out.tokens.back().kind != Tok::Newline)
175 while (indents.size() > 1) {
176 indents.pop_back();
177 push(Tok::Dedent, line, col, 0);
178 }
179 push(Tok::Eof, line, col, 0);
180 }
181
182 /**
183 * Measure one line's indentation and emit INDENT/DEDENT.
184 * @return false on a hard error. Sets at_line_start=false when real content follows.
185 */
186 bool handleLineStart(bool& at_line_start) {
187 uint32_t width = 0;
188 size_t j = i;
189 while (j < src.size()) {
190 char c = src[j];
191 if (c == ' ') {
192 width++;
193 j++;
194 } else if (c == '\t') {
195 fail(Diag::TabIndent, line, width + 1,
196 "tab in indentation — use spaces (a tab is invisible in the editor, and "
197 "mixed indentation silently changes which branch a valve command is in)");
198 return false;
199 } else if (c == '\r') {
200 j++;
201 } else {
202 break;
203 }
204 }
205 // Blank or comment-only line: no NEWLINE, no INDENT/DEDENT — it has no structure.
206 if (j >= src.size() || src[j] == '\n' || src[j] == '#') {
207 while (j < src.size() && src[j] != '\n')
208 j++;
209 if (j < src.size()) {
210 j++; // consume '\n'
211 line++;
212 }
213 i = j;
214 col = 1;
215 at_line_start = true;
216 return true;
217 }
218
219 i = j;
220 col = width + 1;
221 at_line_start = false;
222
223 const uint32_t cur = indents.back();
224 if (width > cur) {
225 if (unit == 0) {
226 const uint32_t u = width - cur;
227 if (u > kMaxIndentUnit) {
228 fail(Diag::BadIndentUnit, line, width + 1,
229 "indent of " + std::to_string(u) + " spaces is wider than the " +
230 std::to_string(kMaxIndentUnit) + "-space maximum");
231 return false;
232 }
233 unit = u;
234 }
235 if (width != cur + unit) {
236 fail(Diag::BadIndentUnit, line, width + 1,
237 "indented " + std::to_string(width - cur) + " spaces; this script indents " +
238 std::to_string(unit) + " at a time, so " + std::to_string(cur + unit) +
239 " was expected");
240 return false;
241 }
242 indents.push_back(width);
243 push(Tok::Indent, line, col, 0);
244 } else if (width < cur) {
245 while (indents.size() > 1 && indents.back() > width) {
246 indents.pop_back();
247 push(Tok::Dedent, line, col, 0);
248 }
249 if (indents.back() != width) {
250 fail(Diag::BadDedent, line, width + 1,
251 "unindent to column " + std::to_string(width + 1) +
252 " does not line up with any enclosing block");
253 return false;
254 }
255 }
256 return true;
257 }
258
259 bool lexToken() {
260 const uint32_t l = line, c = col;
261 const char ch = peek();
262
263 if (ch == '\t') {
264 fail(Diag::UnexpectedChar, l, c, "tab character — use spaces");
265 return false;
266 }
267 // No string literals exist in this language, and refusing the quote outright is what
268 // guarantees a script can never contain ''' — which is what keeps every text-embedding of
269 // a script (TOML literal block included) safe without an escaping rule.
270 if (ch == '\'' || ch == '"') {
272 std::string("quote character — names are bare slugs here, e.g. ") +
273 "open_valve(FUEL_VENT), not open_valve(\"Fuel Vent\")");
274 return false;
275 }
276
277 if (identStart(ch)) {
278 size_t start = i;
279 while (i < src.size() && identChar(src[i])) {
280 i++;
281 col++;
282 }
283 std::string text = src.substr(start, i - start);
284 const Tok k = keywordOf(text);
285 push(k, l, c, static_cast<uint32_t>(text.size()), text);
286 return true;
287 }
288
289 if (ch >= '0' && ch <= '9') {
290 size_t start = i;
291 while (i < src.size() && src[i] >= '0' && src[i] <= '9') {
292 i++;
293 col++;
294 }
295 if (i < src.size() && src[i] == '.') {
296 i++;
297 col++;
298 while (i < src.size() && src[i] >= '0' && src[i] <= '9') {
299 i++;
300 col++;
301 }
302 }
303 // "1e3" must not lex as 1 followed by the name e3. HoldParse records the same trap on
304 // the wire: atoi("1e3") is 1, so a frame asking for a thousand seconds ran a
305 // one-millisecond pulse and the operator weighed a mass against a window that never
306 // happened. Refuse it rather than silently truncating.
307 if (i < src.size() && (identChar(src[i]) || src[i] == '.')) {
308 fail(Diag::BadNumber, l, c,
309 "malformed number — digits and at most one '.', with no exponent and no "
310 "trailing letters");
311 return false;
312 }
313 std::string text = src.substr(start, i - start);
314 push(Tok::Number, l, c, static_cast<uint32_t>(text.size()), text,
315 std::atof(text.c_str()));
316 return true;
317 }
318
319 auto two = [&](char a, char b) {
320 return ch == a && peek(1) == b;
321 };
322
323 Tok k = Tok::Eof;
324 uint32_t len = 1;
325 if (two('<', '=')) {
326 k = Tok::Le;
327 len = 2;
328 } else if (two('>', '=')) {
329 k = Tok::Ge;
330 len = 2;
331 } else if (two('=', '=')) {
332 k = Tok::EqEq;
333 len = 2;
334 } else if (two('!', '=')) {
335 k = Tok::Ne;
336 len = 2;
337 } else {
338 switch (ch) {
339 case ':':
340 k = Tok::Colon;
341 break;
342 case '(':
343 k = Tok::LParen;
344 break;
345 case ')':
346 k = Tok::RParen;
347 break;
348 case ',':
349 k = Tok::Comma;
350 break;
351 case '=':
352 k = Tok::Assign;
353 break;
354 case '+':
355 k = Tok::Plus;
356 break;
357 case '-':
358 k = Tok::Minus;
359 break;
360 case '*':
361 k = Tok::Star;
362 break;
363 case '/':
364 k = Tok::Slash;
365 break;
366 case '<':
367 k = Tok::Lt;
368 break;
369 case '>':
370 k = Tok::Gt;
371 break;
372 default: {
373 std::string what = "'";
374 what += ch;
375 what += "'";
376 fail(Diag::UnexpectedChar, l, c, "unexpected character " + what);
377 return false;
378 }
379 }
380 }
381 i += len;
382 col += len;
383 push(k, l, c, len);
384 return true;
385 }
386};
387
388} // namespace
389
390LexResult lex(const std::string& source) {
391 Lexer lx(source);
392 if (source.size() > kMaxSourceBytes) {
393 lx.out.diagnostics.push_back({Diag::ProgramTooLarge, 1, 1,
394 "script is " + std::to_string(source.size()) +
395 " bytes; the limit is " +
396 std::to_string(kMaxSourceBytes)});
397 return lx.out;
398 }
399 uint32_t lines = 1;
400 for (char c : source)
401 if (c == '\n')
402 lines++;
403 if (lines > kMaxLines) {
404 lx.out.diagnostics.push_back({Diag::TooManyLines, kMaxLines + 1, 1,
405 "script is " + std::to_string(lines) +
406 " lines; the limit is " + std::to_string(kMaxLines)});
407 return lx.out;
408 }
409 lx.run();
410 return lx.out;
411}
412
413} // namespace script
414} // namespace fsw
static void start(gnss_frame_parser_t *p, uint8_t byte)
Definition gnss_frame.c:45
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
Tok keywordOf(const std::string &s)
Definition Lexer.cpp:80
constexpr uint32_t kMaxLines
const char * tok_name(Tok t)
Definition Lexer.cpp:9
LexResult lex(const std::string &source)
Tokenize, emitting INDENT/DEDENT for block structure.
Definition Lexer.cpp:390
Diag
Diagnostic codes.
constexpr uint32_t kMaxIndentUnit
Widest indent unit accepted.
constexpr size_t kMaxSourceBytes
Motorized ball valve position: raw ADC counts -> % open.
void push(Tok k, uint32_t l, uint32_t c, uint32_t len, std::string text={}, double num=0.0)
Definition Lexer.cpp:120
uint32_t unit
Set by the first indented line; every later indent must be a multiple of it.
Definition Lexer.cpp:107
void fail(Diag code, uint32_t l, uint32_t c, std::string msg)
Definition Lexer.cpp:113
std::vector< uint32_t > indents
Column stack for open blocks; [0] is always 0.
Definition Lexer.cpp:105
bool handleLineStart(bool &at_line_start)
Measure one line's indentation and emit INDENT/DEDENT.
Definition Lexer.cpp:186
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")
Definition test_t7.cpp:98