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 newlines
204 if (c == '\n') {
205 tokens.emplace_back(TokenType::newline, "\n", current_position());
206 advance();
207 continue;
208 }
209
210 // Handle comments
211 if (c == '/') {
212 if (peek_next() == '/') {
213 skip_line_comment();
214 continue;
215 }
216 if (peek_next() == '*') {
217 auto result = skip_block_comment();
218 if (!result.has_value()) {
220 }
221 continue;
222 }
223 }
224
225 // Handle preprocessor hash
226 if (c == '#') {
227 tokens.emplace_back(TokenType::hash, "#", current_position());
228 advance();
229 continue;
230 }
231
232 // Handle identifiers and keywords
233 if (is_identifier_start(c)) {
234 tokens.push_back(consume_identifier_or_keyword());
235 continue;
236 }
237
238 // Handle numbers
239 if (is_digit(c)) {
240 auto result = consume_number();
241 if (!result.has_value()) {
243 }
244 tokens.push_back(std::move(result).value());
245 continue;
246 }
247
248 // Handle string literals
249 if (c == '"') {
250 auto result = consume_string();
251 if (!result.has_value()) {
253 }
254 tokens.push_back(std::move(result).value());
255 continue;
256 }
257
258 // Handle operators and delimiters
259 tokens.push_back(consume_operator());
260 }
261
262 tokens.emplace_back(TokenType::end_of_file, "", current_position());
263 return tokens;
264}
265
266char Lexer::peek() const
267{
268 if (is_at_end()) {
269 return '\0';
270 }
271 return content_[current_];
272}
273
274char Lexer::peek_next() const
275{
276 if (current_ + 1 >= content_.size()) {
277 return '\0';
278 }
279 return content_[current_ + 1];
280}
281
282char Lexer::advance()
283{
284 if (is_at_end()) {
285 return '\0';
286 }
287 char c = content_[current_];
288 ++current_;
289 if (c == '\n') {
290 ++line_;
291 column_ = 1;
292 }
293 else {
294 ++column_;
295 }
296 return c;
297}
298
299bool Lexer::is_at_end() const
300{
301 return current_ >= content_.size();
302}
303
304void Lexer::skip_whitespace_except_newline()
305{
306 while (!is_at_end()) {
307 char c = peek();
308 if (c == ' ' || c == '\t' || c == '\r') {
309 advance();
310 }
311 else {
312 break;
313 }
314 }
315}
316
317void Lexer::skip_line_comment()
318{
319 // Skip the //
320 advance();
321 advance();
322
323 while (!is_at_end() && peek() != '\n') {
324 advance();
325 }
326 // Don't consume the newline - it's significant for preprocessor
327}
328
329ChainableResult<void> Lexer::skip_block_comment()
330{
331 SourcePosition start_pos = current_position();
332
333 // Skip the /*
334 advance();
335 advance();
336
337 while (!is_at_end()) {
338 if (peek() == '*' && peek_next() == '/') {
339 advance(); // *
340 advance(); // /
341 return {};
342 }
343 advance();
344 }
345
346 return make_error(start_pos, "unterminated block comment");
347}
348
349Token Lexer::consume_identifier_or_keyword()
350{
351 SourcePosition start_pos = current_position();
352 std::string text;
353
354 while (!is_at_end() && is_identifier_char(peek())) {
355 text += advance();
356 }
357
358 auto it = keywords.find(text);
359 if (it != keywords.end()) {
360 return Token{it->second, std::move(text), start_pos};
361 }
362
363 return Token{TokenType::identifier, std::move(text), start_pos};
364}
365
366ChainableResult<Token> Lexer::consume_number()
367{
368 SourcePosition start_pos = current_position();
369 std::string text;
370
371 // Check for hex, octal, or binary prefix
372 if (peek() == '0' && !is_at_end()) {
373 text += advance();
374 char next = peek();
375
376 // Hexadecimal: 0x or 0X
377 if (next == 'x' || next == 'X') {
378 text += advance();
379 if (!is_hex_digit(peek())) {
380 return make_error(start_pos, format_->format("invalid hexadecimal literal '{}'", text));
381 }
382 while (!is_at_end() && is_hex_digit(peek())) {
383 text += advance();
384 }
385 // Skip optional integer suffix (u, U, l, L, ll, LL, etc.)
386 while (!is_at_end() && (peek() == 'u' || peek() == 'U' || peek() == 'l' || peek() == 'L')) {
387 text += advance();
388 }
389 std::int64_t value = 0;
390 auto [ptr, ec] = std::from_chars(text.data() + 2, text.data() + text.size(), value, 16);
391 if (ec != std::errc{}) {
392 return make_error(start_pos, format_->format("invalid hexadecimal literal '{}'", text));
393 }
394 return Token{std::move(text), value, start_pos};
395 }
396
397 // Binary: 0b or 0B
398 if (next == 'b' || next == 'B') {
399 text += advance();
400 if (!is_binary_digit(peek())) {
401 return make_error(start_pos, format_->format("invalid binary literal '{}'", text));
402 }
403 while (!is_at_end() && is_binary_digit(peek())) {
404 text += advance();
405 }
406 // Skip optional integer suffix
407 while (!is_at_end() && (peek() == 'u' || peek() == 'U' || peek() == 'l' || peek() == 'L')) {
408 text += advance();
409 }
410 std::int64_t value = 0;
411 auto [ptr, ec] = std::from_chars(text.data() + 2, text.data() + text.size(), value, 2);
412 if (ec != std::errc{}) {
413 return make_error(start_pos, format_->format("invalid binary literal '{}'", text));
414 }
415 return Token{std::move(text), value, start_pos};
416 }
417
418 // Octal: starts with 0 followed by octal digits
419 if (is_octal_digit(next)) {
420 while (!is_at_end() && is_octal_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(), text.data() + text.size(), value, 8);
429 if (ec != std::errc{}) {
430 return make_error(start_pos, format_->format("invalid octal literal '{}'", text));
431 }
432 return Token{std::move(text), value, start_pos};
433 }
434
435 // Just a single 0
436 return Token{std::move(text), 0, start_pos};
437 }
438
439 // Decimal number
440 while (!is_at_end() && is_digit(peek())) {
441 text += advance();
442 }
443 // Skip optional integer suffix
444 while (!is_at_end() && (peek() == 'u' || peek() == 'U' || peek() == 'l' || peek() == 'L')) {
445 text += advance();
446 }
447
448 std::int64_t value = 0;
449 auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value, 10);
450 if (ec != std::errc{}) {
451 return make_error(start_pos, format_->format("invalid decimal literal '{}'", text));
452 }
453 return Token{std::move(text), value, start_pos};
454}
455
456ChainableResult<Token> Lexer::consume_string()
457{
458 SourcePosition start_pos = current_position();
459 std::string text;
460 std::string value;
461
462 text += advance(); // Opening quote
463
464 while (!is_at_end() && peek() != '"') {
465 char c = peek();
466
467 // Check for unterminated string (newline without escape)
468 if (c == '\n') {
469 return make_error(start_pos, "unterminated string literal");
470 }
471
472 // Handle escape sequences
473 if (c == '\\' && !is_at_end()) {
474 text += advance(); // backslash
475 if (!is_at_end()) {
476 char escaped = advance();
477 text += escaped;
478 switch (escaped) {
479 case 'n':
480 value += '\n';
481 break;
482 case 't':
483 value += '\t';
484 break;
485 case 'r':
486 value += '\r';
487 break;
488 case '\\':
489 value += '\\';
490 break;
491 case '"':
492 value += '"';
493 break;
494 case '0':
495 value += '\0';
496 break;
497 default:
498 value += escaped; // Unknown escape, keep as-is
499 break;
500 }
501 }
502 }
503 else {
504 text += c;
505 value += c;
506 advance();
507 }
508 }
509
510 if (is_at_end()) {
511 return make_error(start_pos, "unterminated string literal");
512 }
513
514 text += advance(); // Closing quote
515
516 // Create a token that stores the string value in text_
517 // For string literals, we store the unquoted, unescaped value
518 return Token{TokenType::string_literal, std::move(value), start_pos};
519}
520
521Token Lexer::consume_operator()
522{
523 SourcePosition start_pos = current_position();
524 char c = advance();
525
526 switch (c) {
527 case '+':
528 return Token{TokenType::plus, "+", start_pos};
529 case '-':
530 return Token{TokenType::minus, "-", start_pos};
531 case '*':
532 return Token{TokenType::star, "*", start_pos};
533 case '/':
534 return Token{TokenType::slash, "/", start_pos};
535 case '%':
536 return Token{TokenType::percent, "%", start_pos};
537 case '~':
538 return Token{TokenType::tilde, "~", start_pos};
539 case '^':
540 return Token{TokenType::caret, "^", start_pos};
541 case '?':
542 return Token{TokenType::question, "?", start_pos};
543 case ':':
544 return Token{TokenType::colon, ":", start_pos};
545 case '(':
546 return Token{TokenType::left_paren, "(", start_pos};
547 case ')':
548 return Token{TokenType::right_paren, ")", start_pos};
549 case '{':
550 return Token{TokenType::left_brace, "{", start_pos};
551 case '}':
552 return Token{TokenType::right_brace, "}", start_pos};
553 case '[':
554 return Token{TokenType::left_bracket, "[", start_pos};
555 case ']':
556 return Token{TokenType::right_bracket, "]", start_pos};
557 case ',':
558 return Token{TokenType::comma, ",", start_pos};
559 case ';':
560 return Token{TokenType::semicolon, ";", start_pos};
561 case '.':
562 return Token{TokenType::period, ".", start_pos};
563
564 case '<':
565 if (peek() == '<') {
566 advance();
567 return Token{TokenType::less_less, "<<", start_pos};
568 }
569 if (peek() == '=') {
570 advance();
571 return Token{TokenType::less_equal, "<=", start_pos};
572 }
573 return Token{TokenType::less, "<", start_pos};
574
575 case '>':
576 if (peek() == '>') {
577 advance();
578 return Token{TokenType::greater_greater, ">>", start_pos};
579 }
580 if (peek() == '=') {
581 advance();
582 return Token{TokenType::greater_equal, ">=", start_pos};
583 }
584 return Token{TokenType::greater, ">", start_pos};
585
586 case '&':
587 if (peek() == '&') {
588 advance();
589 return Token{TokenType::ampersand_ampersand, "&&", start_pos};
590 }
591 return Token{TokenType::ampersand, "&", start_pos};
592
593 case '|':
594 if (peek() == '|') {
595 advance();
596 return Token{TokenType::pipe_pipe, "||", start_pos};
597 }
598 return Token{TokenType::pipe, "|", start_pos};
599
600 case '=':
601 if (peek() == '=') {
602 advance();
603 return Token{TokenType::equal_equal, "==", start_pos};
604 }
605 return Token{TokenType::equal, "=", start_pos};
606
607 case '!':
608 if (peek() == '=') {
609 advance();
610 return Token{TokenType::exclaim_equal, "!=", start_pos};
611 }
612 return Token{TokenType::exclaim, "!", start_pos};
613
614 default:
615 return Token{TokenType::unknown, std::string(1, c), start_pos};
616 }
617}
618
619SourcePosition Lexer::current_position() const
620{
621 return SourcePosition{line_, column_};
622}
623
624} // 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:63
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:19
Represents a position within source content.
std::size_t line
1-based line number
std::size_t column
1-based column number