Porytiles
Loading...
Searching...
No Matches
lexer.cpp
Go to the documentation of this file.
2
3#include <cctype>
4#include <charconv>
5#include <unordered_map>
6
9
10namespace porytiles {
11
12namespace {
13
14const std::unordered_map<std::string, TokenType> keywords = {
15 {"define", TokenType::kw_define},
16 {"undef", TokenType::kw_undef},
17 {"include", TokenType::kw_include},
18 {"ifdef", TokenType::kw_ifdef},
19 {"ifndef", TokenType::kw_ifndef},
20 {"if", TokenType::kw_if},
21 {"else", TokenType::kw_else},
22 {"elif", TokenType::kw_elif},
23 {"endif", TokenType::kw_endif},
24 {"defined", TokenType::kw_defined},
25 {"pragma", TokenType::kw_pragma},
26 {"enum", TokenType::kw_enum},
27};
28
29[[nodiscard]] bool is_identifier_start(char c)
30{
31 return std::isalpha(static_cast<unsigned char>(c)) != 0 || c == '_';
32}
33
34[[nodiscard]] bool is_identifier_char(char c)
35{
36 return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
37}
38
39[[nodiscard]] bool is_digit(char c)
40{
41 return std::isdigit(static_cast<unsigned char>(c)) != 0;
42}
43
44[[nodiscard]] bool is_hex_digit(char c)
45{
46 return std::isxdigit(static_cast<unsigned char>(c)) != 0;
47}
48
49[[nodiscard]] bool is_octal_digit(char c)
50{
51 return c >= '0' && c <= '7';
52}
53
54[[nodiscard]] bool is_binary_digit(char c)
55{
56 return c == '0' || c == '1';
57}
58
59} // namespace
60
61std::string token_type_name(TokenType type)
62{
63 switch (type) {
65 return "end_of_file";
66 case TokenType::hash:
67 return "#";
69 return "define";
71 return "undef";
73 return "include";
75 return "ifdef";
77 return "ifndef";
79 return "if";
81 return "else";
83 return "elif";
85 return "endif";
87 return "defined";
89 return "pragma";
91 return "enum";
93 return "identifier";
95 return "integer_literal";
97 return "string_literal";
99 return "char_literal";
100 case TokenType::plus:
101 return "+";
102 case TokenType::minus:
103 return "-";
104 case TokenType::star:
105 return "*";
106 case TokenType::slash:
107 return "/";
109 return "%";
111 return "&";
112 case TokenType::pipe:
113 return "|";
114 case TokenType::caret:
115 return "^";
116 case TokenType::tilde:
117 return "~";
119 return "!";
120 case TokenType::less:
121 return "<";
123 return ">";
124 case TokenType::equal:
125 return "=";
127 return "?";
128 case TokenType::colon:
129 return ":";
131 return "<<";
133 return ">>";
135 return "&&";
137 return "||";
139 return "==";
141 return "!=";
143 return "<=";
145 return ">=";
147 return "(";
149 return ")";
151 return "{";
153 return "}";
155 return "[";
157 return "]";
158 case TokenType::comma:
159 return ",";
161 return ";";
163 return ".";
165 return "newline";
167 return "unknown";
168 }
169 return "unknown";
170}
171
172Lexer::Lexer(gsl::not_null<const TextFormatter *> format, std::string content)
173 : format_{format}, content_{std::move(content)}
174{
175}
176
177Lexer::Lexer(gsl::not_null<const TextFormatter *> format, std::string content, const CParserContext *context)
178 : format_{format}, content_{std::move(content)}, context_{context}
179{
180}
181
182FormattableError Lexer::make_error(SourcePosition pos, std::string message) const
183{
184 if (context_ != nullptr) {
185 return context_->make_error(pos, std::move(message));
186 }
187 return FormattableError{format_->format("{}:{}: {}", pos.line, pos.column, message)};
188}
189
191{
192 std::vector<Token> tokens;
193
194 while (!is_at_end()) {
195 skip_whitespace_except_newline();
196
197 if (is_at_end()) {
198 break;
199 }
200
201 char c = peek();
202
203 // Handle line continuation: a backslash immediately before a newline splices the two physical lines into one
204 // logical line. Emit no token and consume the backslash together with the following newline (handling CRLF).
205 if (c == '\\') {
206 char next = peek_next();
207 if (next == '\n') {
208 advance(); // backslash
209 advance(); // newline
210 continue;
211 }
212 if (next == '\r' && current_ + 2 < content_.size() && content_[current_ + 2] == '\n') {
213 advance(); // backslash
214 advance(); // carriage return
215 advance(); // newline
216 continue;
217 }
218 }
219
220 // Handle newlines
221 if (c == '\n') {
222 tokens.emplace_back(TokenType::newline, "\n", current_position());
223 advance();
224 continue;
225 }
226
227 // Handle comments
228 if (c == '/') {
229 if (peek_next() == '/') {
230 skip_line_comment();
231 continue;
232 }
233 if (peek_next() == '*') {
234 auto result = skip_block_comment();
235 if (!result.has_value()) {
237 }
238 continue;
239 }
240 }
241
242 // Handle preprocessor hash
243 if (c == '#') {
244 tokens.emplace_back(TokenType::hash, "#", current_position());
245 advance();
246 continue;
247 }
248
249 // Handle identifiers and keywords
250 if (is_identifier_start(c)) {
251 tokens.push_back(consume_identifier_or_keyword());
252 continue;
253 }
254
255 // Handle numbers
256 if (is_digit(c)) {
257 auto result = consume_number();
258 if (!result.has_value()) {
260 }
261 tokens.push_back(std::move(result).value());
262 continue;
263 }
264
265 // Handle string literals
266 if (c == '"') {
267 auto result = consume_string();
268 if (!result.has_value()) {
270 }
271 tokens.push_back(std::move(result).value());
272 continue;
273 }
274
275 // Handle operators and delimiters
276 tokens.push_back(consume_operator());
277 }
278
279 tokens.emplace_back(TokenType::end_of_file, "", current_position());
280 return tokens;
281}
282
283char Lexer::peek() const
284{
285 if (is_at_end()) {
286 return '\0';
287 }
288 return content_[current_];
289}
290
291char Lexer::peek_next() const
292{
293 if (current_ + 1 >= content_.size()) {
294 return '\0';
295 }
296 return content_[current_ + 1];
297}
298
299char Lexer::advance()
300{
301 if (is_at_end()) {
302 return '\0';
303 }
304 char c = content_[current_];
305 ++current_;
306 if (c == '\n') {
307 ++line_;
308 column_ = 1;
309 }
310 else {
311 ++column_;
312 }
313 return c;
314}
315
316bool Lexer::is_at_end() const
317{
318 return current_ >= content_.size();
319}
320
321void Lexer::skip_whitespace_except_newline()
322{
323 while (!is_at_end()) {
324 char c = peek();
325 if (c == ' ' || c == '\t' || c == '\r') {
326 advance();
327 }
328 else {
329 break;
330 }
331 }
332}
333
334void Lexer::skip_line_comment()
335{
336 // Skip the //
337 advance();
338 advance();
339
340 while (!is_at_end() && peek() != '\n') {
341 advance();
342 }
343 // Don't consume the newline - it's significant for preprocessor
344}
345
346ChainableResult<void> Lexer::skip_block_comment()
347{
348 SourcePosition start_pos = current_position();
349
350 // Skip the /*
351 advance();
352 advance();
353
354 while (!is_at_end()) {
355 if (peek() == '*' && peek_next() == '/') {
356 advance(); // *
357 advance(); // /
358 return {};
359 }
360 advance();
361 }
362
363 return make_error(start_pos, "unterminated block comment");
364}
365
366Token Lexer::consume_identifier_or_keyword()
367{
368 SourcePosition start_pos = current_position();
369 std::string text;
370
371 while (!is_at_end() && is_identifier_char(peek())) {
372 text += advance();
373 }
374
375 auto it = keywords.find(text);
376 if (it != keywords.end()) {
377 return Token{it->second, std::move(text), start_pos};
378 }
379
380 return Token{TokenType::identifier, std::move(text), start_pos};
381}
382
383ChainableResult<Token> Lexer::consume_number()
384{
385 SourcePosition start_pos = current_position();
386 std::string text;
387
388 // Check for hex, octal, or binary prefix
389 if (peek() == '0' && !is_at_end()) {
390 text += advance();
391 char next = peek();
392
393 // Hexadecimal: 0x or 0X
394 if (next == 'x' || next == 'X') {
395 text += advance();
396 if (!is_hex_digit(peek())) {
397 return make_error(start_pos, format_->format("invalid hexadecimal literal '{}'", text));
398 }
399 while (!is_at_end() && is_hex_digit(peek())) {
400 text += advance();
401 }
402 // Skip optional integer suffix (u, U, l, L, ll, LL, etc.)
403 while (!is_at_end() && (peek() == 'u' || peek() == 'U' || peek() == 'l' || peek() == 'L')) {
404 text += advance();
405 }
406 std::int64_t value = 0;
407 auto [ptr, ec] = std::from_chars(text.data() + 2, text.data() + text.size(), value, 16);
408 if (ec != std::errc{}) {
409 return make_error(start_pos, format_->format("invalid hexadecimal literal '{}'", text));
410 }
411 return Token{std::move(text), value, start_pos};
412 }
413
414 // Binary: 0b or 0B
415 if (next == 'b' || next == 'B') {
416 text += advance();
417 if (!is_binary_digit(peek())) {
418 return make_error(start_pos, format_->format("invalid binary literal '{}'", text));
419 }
420 while (!is_at_end() && is_binary_digit(peek())) {
421 text += advance();
422 }
423 // Skip optional integer suffix
424 while (!is_at_end() && (peek() == 'u' || peek() == 'U' || peek() == 'l' || peek() == 'L')) {
425 text += advance();
426 }
427 std::int64_t value = 0;
428 auto [ptr, ec] = std::from_chars(text.data() + 2, text.data() + text.size(), value, 2);
429 if (ec != std::errc{}) {
430 return make_error(start_pos, format_->format("invalid binary literal '{}'", text));
431 }
432 return Token{std::move(text), value, start_pos};
433 }
434
435 // Octal: starts with 0 followed by octal digits
436 if (is_octal_digit(next)) {
437 while (!is_at_end() && is_octal_digit(peek())) {
438 text += advance();
439 }
440 // Skip optional integer suffix
441 while (!is_at_end() && (peek() == 'u' || peek() == 'U' || peek() == 'l' || peek() == 'L')) {
442 text += advance();
443 }
444 std::int64_t value = 0;
445 auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value, 8);
446 if (ec != std::errc{}) {
447 return make_error(start_pos, format_->format("invalid octal literal '{}'", text));
448 }
449 return Token{std::move(text), value, start_pos};
450 }
451
452 // Just a single 0
453 return Token{std::move(text), 0, start_pos};
454 }
455
456 // Decimal number
457 while (!is_at_end() && is_digit(peek())) {
458 text += advance();
459 }
460 // Skip optional integer suffix
461 while (!is_at_end() && (peek() == 'u' || peek() == 'U' || peek() == 'l' || peek() == 'L')) {
462 text += advance();
463 }
464
465 std::int64_t value = 0;
466 auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value, 10);
467 if (ec != std::errc{}) {
468 return make_error(start_pos, format_->format("invalid decimal literal '{}'", text));
469 }
470 return Token{std::move(text), value, start_pos};
471}
472
473ChainableResult<Token> Lexer::consume_string()
474{
475 SourcePosition start_pos = current_position();
476 std::string text;
477 std::string value;
478
479 text += advance(); // Opening quote
480
481 while (!is_at_end() && peek() != '"') {
482 char c = peek();
483
484 // Check for unterminated string (newline without escape)
485 if (c == '\n') {
486 return make_error(start_pos, "unterminated string literal");
487 }
488
489 // Handle escape sequences
490 if (c == '\\' && !is_at_end()) {
491 text += advance(); // backslash
492 if (!is_at_end()) {
493 char escaped = advance();
494 text += escaped;
495 switch (escaped) {
496 case 'n':
497 value += '\n';
498 break;
499 case 't':
500 value += '\t';
501 break;
502 case 'r':
503 value += '\r';
504 break;
505 case '\\':
506 value += '\\';
507 break;
508 case '"':
509 value += '"';
510 break;
511 case '0':
512 value += '\0';
513 break;
514 default:
515 value += escaped; // Unknown escape, keep as-is
516 break;
517 }
518 }
519 }
520 else {
521 text += c;
522 value += c;
523 advance();
524 }
525 }
526
527 if (is_at_end()) {
528 return make_error(start_pos, "unterminated string literal");
529 }
530
531 text += advance(); // Closing quote
532
533 // Create a token that stores the string value in text_
534 // For string literals, we store the unquoted, unescaped value
535 return Token{TokenType::string_literal, std::move(value), start_pos};
536}
537
538Token Lexer::consume_operator()
539{
540 SourcePosition start_pos = current_position();
541 char c = advance();
542
543 switch (c) {
544 case '+':
545 return Token{TokenType::plus, "+", start_pos};
546 case '-':
547 return Token{TokenType::minus, "-", start_pos};
548 case '*':
549 return Token{TokenType::star, "*", start_pos};
550 case '/':
551 return Token{TokenType::slash, "/", start_pos};
552 case '%':
553 return Token{TokenType::percent, "%", start_pos};
554 case '~':
555 return Token{TokenType::tilde, "~", start_pos};
556 case '^':
557 return Token{TokenType::caret, "^", start_pos};
558 case '?':
559 return Token{TokenType::question, "?", start_pos};
560 case ':':
561 return Token{TokenType::colon, ":", start_pos};
562 case '(':
563 return Token{TokenType::left_paren, "(", start_pos};
564 case ')':
565 return Token{TokenType::right_paren, ")", start_pos};
566 case '{':
567 return Token{TokenType::left_brace, "{", start_pos};
568 case '}':
569 return Token{TokenType::right_brace, "}", start_pos};
570 case '[':
571 return Token{TokenType::left_bracket, "[", start_pos};
572 case ']':
573 return Token{TokenType::right_bracket, "]", start_pos};
574 case ',':
575 return Token{TokenType::comma, ",", start_pos};
576 case ';':
577 return Token{TokenType::semicolon, ";", start_pos};
578 case '.':
579 return Token{TokenType::period, ".", start_pos};
580
581 case '<':
582 if (peek() == '<') {
583 advance();
584 return Token{TokenType::less_less, "<<", start_pos};
585 }
586 if (peek() == '=') {
587 advance();
588 return Token{TokenType::less_equal, "<=", start_pos};
589 }
590 return Token{TokenType::less, "<", start_pos};
591
592 case '>':
593 if (peek() == '>') {
594 advance();
595 return Token{TokenType::greater_greater, ">>", start_pos};
596 }
597 if (peek() == '=') {
598 advance();
599 return Token{TokenType::greater_equal, ">=", start_pos};
600 }
601 return Token{TokenType::greater, ">", start_pos};
602
603 case '&':
604 if (peek() == '&') {
605 advance();
606 return Token{TokenType::ampersand_ampersand, "&&", start_pos};
607 }
608 return Token{TokenType::ampersand, "&", start_pos};
609
610 case '|':
611 if (peek() == '|') {
612 advance();
613 return Token{TokenType::pipe_pipe, "||", start_pos};
614 }
615 return Token{TokenType::pipe, "|", start_pos};
616
617 case '=':
618 if (peek() == '=') {
619 advance();
620 return Token{TokenType::equal_equal, "==", start_pos};
621 }
622 return Token{TokenType::equal, "=", start_pos};
623
624 case '!':
625 if (peek() == '=') {
626 advance();
627 return Token{TokenType::exclaim_equal, "!=", start_pos};
628 }
629 return Token{TokenType::exclaim, "!", start_pos};
630
631 default:
632 return Token{TokenType::unknown, std::string(1, c), start_pos};
633 }
634}
635
636SourcePosition Lexer::current_position() const
637{
638 return SourcePosition{line_, column_};
639}
640
641} // namespace porytiles
Context object providing rich error formatting for C/C++ parsing.
FormattableError make_error(SourcePosition pos, const std::string &message) const
Creates a FormattableError with source context.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
ChainableResult< std::vector< Token > > lex()
Tokenizes the entire source content.
Definition lexer.cpp:190
Lexer(gsl::not_null< const TextFormatter * > format, std::string content)
Constructs a lexer for the given source content.
Definition lexer.cpp:172
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
std::string token_type_name(TokenType type)
Returns a human-readable name for a token type.
Definition lexer.cpp:61
TokenType
Enumeration of token types recognized by the C parser lexer.
Definition token.hpp:17
Represents a position within source content.
std::size_t line
1-based line number
std::size_t column
1-based column number