SciLex
A header-only C++20 lexer built on REAL
Loading...
Searching...
No Matches
lexer.hpp
Go to the documentation of this file.
1
17#ifndef SCILEX_LEXER_HPP
18#define SCILEX_LEXER_HPP
19
20#include <algorithm>
21#include <array>
22#include <iterator>
23#include <map>
24#include <memory>
25#include <optional>
26#include <random>
27#include <span>
28#include <stdexcept>
29#include <string>
30#include <string_view>
31#include <tuple>
32#include <utility>
33#include <vector>
34
35#include <real/dfa.hpp>
36#include <real/real.hpp>
37
38#include "token.hpp"
39
40namespace scilex {
41
42 class token_iterator;
43 class token_range;
44
52 enum class eof_policy
53 {
54 omit,
55 append,
56 };
57
63 {
65 enum class op
66 {
67 push,
68 pop,
69 set,
70 };
71
73 std::string target {};
74
79 std::size_t target_id {0};
80 };
81
108 struct rule
109 {
110 int kind;
111 real::regex pattern;
112 bool skip {false};
113 std::vector<std::string> in_mode {};
114 std::optional<mode_action> action {};
115 };
116
122 class lex_error : public std::runtime_error
123 {
124 public:
125
131 lex_error(const std::string& message,
133 : std::runtime_error(message),
135 {}
136
140 [[nodiscard]] position where() const noexcept
141 {
142 return where_;
143 }
144
145 private:
146
148 };
149
154 struct frame
155 {
156 std::size_t mode_id;
158 };
159
173 inline void apply_transition(const rule& r,
174 position start,
175 std::vector<frame>& stack)
176 {
177 if (!r.action) {
178 return;
179 }
180 if (r.action->operation == mode_action::op::push) {
181 stack.push_back(frame {.mode_id = r.action->target_id, .entry_pos = start});
182 }
183 else if (r.action->operation == mode_action::op::pop) {
184 if (stack.size() == 1) {
185 throw lex_error("cannot pop the mode stack: already at the root mode", start);
186 }
187 stack.pop_back();
188 }
189 else { // set: replace the active mode in place (depth unchanged)
190 stack.back().mode_id = r.action->target_id;
191 }
192 }
193
199 enum class error_policy
200 {
201 raise,
206 token,
207 };
208
220 enum class column_unit
221 {
222 bytes,
223 codepoints,
224 utf16,
225 };
226
234 class lexer
235 {
236 public:
237
262 explicit lexer(std::vector<rule> rules,
263 std::vector<std::string> insignificant_modes = {},
264 std::vector<std::string> dfa_modes = {},
267 : rules_(std::move(rules)),
268 errors_(errors),
270 {
272 build_significance(insignificant_modes);
273 build_dfa_modes(dfa_modes);
274 }
275
278 [[nodiscard]] column_unit columns() const noexcept
279 {
280 return columns_;
281 }
282
298 [[nodiscard]] std::vector<token> tokenize(std::string_view source,
299 eof_policy policy = eof_policy::omit) const
300 {
301 std::vector<token> out;
302 position cursor {0, 1, 1};
303 std::vector<frame> stack {frame {.mode_id = 0, .entry_pos = cursor}}; // start in "default"
304 token next {};
305 while (scan_next(source, cursor, stack, next)) {
306 out.push_back(next);
307 }
308 if (policy == eof_policy::append) {
309 // The cursor now sits at the end position (past any trailing trivia).
310 out.push_back(token {end_of_input, source.substr(cursor.offset), cursor});
311 }
312 return out;
313 }
314
328 [[nodiscard]] token_range scan(std::string_view source,
329 eof_policy policy = eof_policy::omit) const&;
330
332 token_range scan(std::string_view source,
333 eof_policy policy = eof_policy::omit) const&& = delete;
334
338 [[nodiscard]] const std::vector<bool>& mode_significant() const noexcept
339 {
340 return mode_significant_;
341 }
342
344 [[nodiscard]] const std::string& mode_name(std::size_t id) const noexcept
345 {
346 return mode_names_[id];
347 }
348
356 [[nodiscard]] std::vector<std::string> dfa_modes_active() const
357 {
358 std::vector<std::string> active;
359 for (std::size_t m {0}; m < per_mode_dfa_.size(); ++m) {
360 if (per_mode_dfa_[m]) {
361 active.push_back(mode_names_[m]);
362 }
363 }
364 return active;
365 }
366
367 private:
368
369 friend class token_iterator;
370
372 static std::string position_label(position where)
373 {
374 return std::to_string(where.line) + ":" + std::to_string(where.column);
375 }
376
396 bool scan_next(std::string_view source,
397 position& cursor,
398 std::vector<frame>& stack,
399 token& out) const
400 {
401 while (cursor.offset < source.size()) {
402 const std::string_view rest {source.substr(cursor.offset)};
403 const std::size_t mode {stack.back().mode_id};
404 const munch_result m {munch_at(mode, rest, static_cast<unsigned char>(source[cursor.offset]))};
405
406 if (!m.have) {
408 throw lex_error("no rule matches in mode '" + mode_names_[mode] + "' (entered at "
409 + position_label(stack.back().entry_pos) + ")", cursor); // #1
410 }
411 // Recovery (error_policy::token): accumulate the maximal run of bytes that no rule in this
412 // mode can begin into ONE reserved-kind error token, then resume — no throw, no transition
413 // (the run stays in its mode). The run ends at the first position where a rule matches (>0);
414 // may_start is an O(1) first-byte pre-filter that skips the bulk of the noise without a full
415 // match attempt. The lexeme is the exact offending bytes.
416 const position err_start {cursor};
417 advance(source, cursor, 1); // the byte at err_start is unmatched by definition
418 while (cursor.offset < source.size()
419 && !starts_a_match(mode, source, cursor.offset)) {
420 advance(source, cursor, 1);
421 }
422 out = token {scilex::error, source.substr(err_start.offset, cursor.offset - err_start.offset),
423 err_start, mode};
424 return true;
425 }
426 if (m.len == 0) {
427 // A rule won with a zero-length match (a nullable rule and no longer match at this
428 // position). Advancing by 0 would spin forever, so report it as a lexical error — fatal
429 // under either policy (recovery cannot make progress here). The shared advance point, so
430 // both the Pike and DFA paths are covered.
431 throw lex_error("zero-length match in mode '" + mode_names_[mode]
432 + "' (rule never advances)", cursor); // #4
433 }
434
435 const std::size_t best_idx {m.idx};
436 const std::size_t best_len {m.len};
437 const position start {cursor};
438 advance(source, cursor, best_len);
439
440 apply_transition(rules_[best_idx], start, stack); // advances, then transitions (#2 on a bad pop)
441
442 if (!rules_[best_idx].skip) {
443 // Tag the token with the mode it was lexed in (captured before the
444 // transition above) — Layout Awareness reads it; the scan is untouched.
445 out = token {rules_[best_idx].kind, source.substr(start.offset, best_len), start, mode};
446 return true;
447 }
448 // Skip rule: keep scanning for the next emitted token (possibly in a new mode).
449 }
450 if (stack.size() > 1) {
452 throw lex_error("unterminated mode '" + mode_names_[stack.back().mode_id] + "' (entered at "
453 + position_label(stack.back().entry_pos) + ")", stack.back().entry_pos); // #3
454 }
455 // Recovery (error_policy::token): a mode was still pushed at end of input. Emit one zero-width
456 // error token positioned at the EOF (the partial tokens already emitted stay), then unwind to
457 // the root so the next call reports a clean end of input.
458 out = token {scilex::error, source.substr(cursor.offset, 0), cursor, stack.back().mode_id};
459 stack.resize(1);
460 return true;
461 }
462 return false;
463 }
464
466 std::size_t intern_mode(const std::string& name)
467 {
468 const auto [it, inserted] {mode_id_.emplace(name, mode_names_.size())};
469 if (inserted) {
470 mode_names_.push_back(name);
471 }
472 return it->second;
473 }
474
478 void add_to_mode(std::size_t m,
479 std::size_t idx)
480 {
481 const real::regex& pattern {rules_[idx].pattern};
482 dispatch& target {per_mode_[m]};
483 if (!pattern.has_first_byte_set()) {
484 target.general.push_back(idx);
485 }
486 else if (const std::optional<unsigned char> byte {pattern.unique_first_byte()}) {
487 target.first_byte_index[*byte].push_back(idx);
488 }
489 else {
490 for (int candidate {0}; candidate < 256; ++candidate) {
491 if (pattern.may_start_with(static_cast<unsigned char>(candidate))) {
492 target.first_byte_index[static_cast<unsigned char>(candidate)].push_back(idx);
493 }
494 }
495 }
496 }
497
499 [[nodiscard]] bool mode_is_empty(std::size_t m) const
500 {
501 const dispatch& d {per_mode_[m]};
502 return d.general.empty()
503 && std::all_of(d.first_byte_index.begin(), d.first_byte_index.end(),
504 [](const std::vector<std::size_t>& bucket) { return bucket.empty(); });
505 }
506
511 {
512 for (const rule& candidate : rules_) {
513 if (!candidate.action) {
514 continue;
515 }
516 if (candidate.pattern.pattern().empty()) {
517 throw std::invalid_argument("a transition rule must consume input (empty pattern)");
518 }
519 if (candidate.action->operation != mode_action::op::pop
520 && mode_is_empty(mode_id_.at(candidate.action->target))) {
521 throw std::invalid_argument("a transition targets the empty mode '"
522 + candidate.action->target + "' (no rule is active in it)");
523 }
524 }
525 }
526
536 {
537 intern_mode("default"); // mode 0, always present
538 for (const rule& candidate : rules_) {
539 for (const std::string& name : candidate.in_mode) {
540 intern_mode(name);
541 }
542 if (const std::optional<mode_action> action {candidate.action};
543 action.has_value() && action->operation != mode_action::op::pop) {
544 intern_mode(action->target);
545 }
546 }
547 per_mode_.resize(mode_names_.size());
548
549 for (std::size_t idx {0}; idx < rules_.size(); ++idx) {
550 if (rules_[idx].in_mode.empty()) {
551 add_to_mode(0, idx); // an undeclared rule is active in "default" only
552 }
553 else {
554 for (const std::string& name : rules_[idx].in_mode) {
555 add_to_mode(mode_id_.at(name), idx);
556 }
557 }
558 }
560
561 // Pre-resolve each transition's target mode id once, now that every mode is
562 // interned and validated, so the per-token apply_transition reads a field instead
563 // of a name→id map lookup. The target string stays for diagnostics; pop has none.
564 for (rule& candidate : rules_) {
565 if (candidate.action && candidate.action->operation != mode_action::op::pop) {
566 candidate.action->target_id = mode_id_.at(candidate.action->target);
567 }
568 }
569 }
570
575 void build_significance(const std::vector<std::string>& insignificant_modes)
576 {
577 if (insignificant_modes.empty()) {
578 return;
579 }
580 mode_significant_.assign(mode_names_.size(), true);
581 for (const std::string& name : insignificant_modes) {
582 const auto found {mode_id_.find(name)};
583 if (found == mode_id_.end()) {
584 throw std::invalid_argument("insignificant_modes names an unknown mode: " + name);
585 }
586 mode_significant_[found->second] = false;
587 }
588 }
589
591 struct mode_dfa
592 {
593 real::dfa dfa;
594 std::vector<std::size_t> to_global;
595 };
596
600 {
601 bool have {false};
602 std::size_t idx {0};
603 std::size_t len {0};
604 };
605
610 munch_result munch_at(std::size_t mode,
611 std::string_view rest,
612 unsigned char lead) const
613 {
614 if (per_mode_dfa_[mode]) {
615 if (const std::optional<real::dfa_match> matched {per_mode_dfa_[mode]->dfa.match(rest)}) {
616 return munch_result {.have = true, .idx = per_mode_dfa_[mode]->to_global[matched->rule_index],
617 .len = matched->length};
618 }
619 return munch_result {};
620 }
621 return pike_munch_in_mode(mode, rest, lead);
622 }
623
632 bool may_start(std::size_t mode,
633 unsigned char byte) const
634 {
635 return !per_mode_[mode].first_byte_index[byte].empty();
636 }
637
642 bool starts_a_match(std::size_t mode,
643 std::string_view source,
644 std::size_t offset) const
645 {
646 const unsigned char lead {static_cast<unsigned char>(source[offset])};
647 if (!may_start(mode, lead)) {
648 return false;
649 }
650 return munch_at(mode, source.substr(offset), lead).have;
651 }
652
656 void advance(std::string_view source,
657 position& cursor,
658 std::size_t n) const
659 {
660 for (std::size_t i {0}; i < n; ++i) {
661 if (source[cursor.offset] == '\n') {
662 ++cursor.line;
663 cursor.column = 1;
664 }
665 else {
666 cursor.column += column_step(source, cursor.offset, columns_);
667 }
668 ++cursor.offset;
669 }
670 }
671
675 static std::size_t valid_utf8_len(std::string_view s,
676 std::size_t off)
677 {
678 const unsigned char b0 {static_cast<unsigned char>(s[off])};
679 std::size_t len {0};
680 unsigned int cp {0};
681 if (b0 < 0x80U) {
682 return 1; // ASCII
683 }
684 if ((b0 & 0xE0U) == 0xC0U) {
685 len = 2;
686 cp = b0 & 0x1FU;
687 }
688 else if ((b0 & 0xF0U) == 0xE0U) {
689 len = 3;
690 cp = b0 & 0x0FU;
691 }
692 else if ((b0 & 0xF8U) == 0xF0U) {
693 len = 4;
694 cp = b0 & 0x07U;
695 }
696 else {
697 return 0; // a continuation byte (0x80–0xBF) or an invalid lead (0xF8–0xFF)
698 }
699 if (off + len > s.size()) {
700 return 0; // truncated
701 }
702 for (std::size_t i {1}; i < len; ++i) {
703 const unsigned char bi {static_cast<unsigned char>(s[off + i])};
704 if ((bi & 0xC0U) != 0x80U) {
705 return 0; // a missing continuation
706 }
707 cp = (cp << 6U) | (bi & 0x3FU);
708 }
709 static constexpr unsigned int min_for_len[5] {0, 0, 0x80U, 0x800U, 0x10000U};
710 if (cp < min_for_len[len] || (cp >= 0xD800U && cp <= 0xDFFFU) || cp > 0x10FFFFU) {
711 return 0; // overlong, a UTF-16 surrogate, or beyond U+10FFFF
712 }
713 return len;
714 }
715
722 static std::size_t column_step(std::string_view source,
723 std::size_t off,
725 {
726 if (unit == scilex::column_unit::bytes) {
727 return 1;
728 }
729 const unsigned char byte {static_cast<unsigned char>(source[off])};
730 if ((byte & 0xC0U) == 0x80U) { // a continuation byte
731 // Score 0 only if it belongs to a valid codepoint whose lead is 1–3 bytes back; an orphan
732 // continuation is malformed and scores 1. (A codepoint never spans a newline, so this
733 // fixed look-back cannot cross a line boundary in a way that matters.)
734 for (std::size_t back {1}; back <= 3 && back <= off; ++back) {
735 if (valid_utf8_len(source, off - back) > back) {
736 return 0;
737 }
738 }
739 return 1;
740 }
741 if (unit == scilex::column_unit::utf16) {
742 return valid_utf8_len(source, off) == 4 ? 2 : 1; // an astral codepoint is a surrogate pair
743 }
744 return 1; // codepoints: an ASCII byte or a lead (its continuations already scored 0)
745 }
746
753 std::string_view rest,
754 unsigned char lead) const
755 {
756 std::size_t best_len {0};
757 std::size_t best_idx {0};
758 bool have {false};
759 const auto consider {[&](std::size_t idx) {
760 // idx comes from this mode's first-byte dispatch, populated
761 // in build_dispatch() from rules_ indices, so it is always in
762 // range. The analyzer cannot prove that cross-vector invariant
763 // once this munch is a standalone shared method; a bounds guard
764 // would be an unreachable branch the 100%-4D gate rejects, so the
765 // proven false positive is suppressed here (see REPORT note).
766 // NOLINTNEXTLINE(clang-analyzer-core.NonNullParamChecker)
767 const auto matched {rules_[idx].pattern.match(rest)};
768 // A zero-length match participates (it can only win when no rule
769 // matches >0 here); the shared guard in scan_next turns that win
770 // into a lexical error rather than a stalled scan. Maximal munch
771 // still prefers any longer non-empty match.
772 if (matched
773 && (!have || matched.end() > best_len
774 || (matched.end() == best_len && idx < best_idx))) {
775 best_len = matched.end();
776 best_idx = idx;
777 have = true;
778 }
779 }};
780 const dispatch& active {per_mode_[mode]};
781 for (const std::size_t idx : active.first_byte_index[lead]) {
782 consider(idx);
783 }
784 for (const std::size_t idx : active.general) {
785 consider(idx);
786 }
787 return {.have = have, .idx = best_idx, .len = best_len};
788 }
789
792 [[nodiscard]] bool rule_active_in_mode(std::size_t idx,
793 std::size_t mode) const
794 {
795 const std::vector<std::string>& modes {rules_[idx].in_mode};
796 if (modes.empty()) {
797 return mode == 0;
798 }
799 for (const std::string& name : modes) {
800 if (mode_id_.at(name) == mode) {
801 return true;
802 }
803 }
804 return false;
805 }
806
811 std::vector<std::string> audit_probes(const std::vector<std::size_t>& to_global) const
812 {
813 std::array<bool, 256> seen {};
814 std::vector<unsigned char> alpha;
815 const auto add {[&](unsigned char b) {
816 if (!seen[b]) {
817 seen[b] = true;
818 alpha.push_back(b);
819 }
820 }};
821 for (const std::size_t g : to_global) {
822 for (int b {0}; b < 256; ++b) {
823 if (rules_[g].pattern.may_start_with(static_cast<unsigned char>(b))) {
824 add(static_cast<unsigned char>(b));
825 }
826 }
827 }
828 for (const char structural : std::string_view {" \t\n\"'/*-+=<>()[]{};.:,aAz09_"}) {
829 add(static_cast<unsigned char>(structural));
830 }
831
832 // alpha is always non-empty (the structural bytes above are unconditional), so
833 // the probe count is O(alphabet) + a fixed random batch — deterministic, bounded,
834 // and free of cap branches. Singletons + short repeats expose lazy delimiters and
835 // quantifier boundaries (the hard cases); the random batch broadens coverage.
836 std::vector<std::string> probes;
837 for (const unsigned char b : alpha) {
838 for (const std::size_t n : std::array<std::size_t, 5> {1, 2, 3, 6, 8}) {
839 probes.emplace_back(n, static_cast<char>(b));
840 }
841 }
842 // Fixed seed by design: this RNG only generates local probe strings for the
843 // build-time DFA equivalence audit, which must be reproducible. No security
844 // role (no tokens, crypto or identifiers) — a constant seed is correct here.
845 // NOLINTNEXTLINE(bugprone-random-generator-seed,cert-msc32-c,cert-msc51-cpp)
846 std::mt19937 rng {0x5C11EFU}; // fixed seed: the audit is reproducible
847 std::uniform_int_distribution<std::size_t> len_d {1, 48};
848 std::uniform_int_distribution<std::size_t> sym_d {0, alpha.size() - 1};
849 for (int batch {0}; batch < 256; ++batch) {
850 std::string input;
851 const std::size_t len {len_d(rng)};
852 for (std::size_t i {0}; i < len; ++i) {
853 input.push_back(static_cast<char>(alpha[sym_d(rng)]));
854 }
855 probes.push_back(std::move(input));
856 }
857 return probes;
858 }
859
863 [[nodiscard]] bool audit_passes(const real::dfa& candidate,
864 const std::vector<std::size_t>& to_global,
865 std::size_t mode) const
866 {
867 const std::vector<std::string> probes {audit_probes(to_global)};
868 for (const std::string& probe : probes) {
869 const std::string_view rest {probe}; // probes always have length >= 1
870 const std::optional<real::dfa_match> hit {candidate.match(rest)};
871 const munch_result pike {pike_munch_in_mode(mode, rest, static_cast<unsigned char>(rest[0]))};
872 std::size_t dfa_idx {0};
873 std::size_t dfa_len {0};
874 if (hit.has_value()) {
875 dfa_idx = to_global[hit->rule_index];
876 dfa_len = hit->length;
877 }
878 // One comparison — the tuple's element-wise short-circuit lives in <tuple>, not
879 // here — so any divergence (chiefly a lazy rule's shortest-vs-longest) rejects.
880 if (std::tuple {hit.has_value(), dfa_idx, dfa_len} != std::tuple {pike.have, pike.idx, pike.len}) {
881 return false;
882 }
883 }
884 return true;
885 }
886
896 std::optional<mode_dfa> try_build_mode_dfa(std::vector<std::size_t> to_global,
897 std::size_t mode)
898 {
899 std::vector<real::detail::program_view> programs;
900 programs.reserve(to_global.size());
901 for (const std::size_t g : to_global) {
902 programs.push_back(rules_[g].pattern.raw_program());
903 }
904 try {
905 real::dfa candidate {std::span<const real::detail::program_view>(programs)};
906 if (!audit_passes(candidate, to_global, mode)) {
907 return std::nullopt; // a divergence (e.g. a lazy rule) → keep this mode on Pike
908 }
909 return mode_dfa {.dfa = std::move(candidate), .to_global = std::move(to_global)};
910 }
911 catch (const real::dfa_error&) {
912 return std::nullopt; // un-DFA-able assertion ($, \b, multiline ^/$): keep on Pike
913 }
914 }
915
924 void build_dfa_modes(const std::vector<std::string>& dfa_modes)
925 {
926 per_mode_dfa_.assign(mode_names_.size(), nullptr);
927 for (const std::string& name : dfa_modes) {
928 const auto found {mode_id_.find(name)};
929 if (found == mode_id_.end()) {
930 throw std::invalid_argument("dfa_modes names an unknown mode: " + name);
931 }
932 const std::size_t mode {found->second};
933 std::vector<std::size_t> to_global;
934 for (std::size_t idx {0}; idx < rules_.size(); ++idx) {
935 if (rule_active_in_mode(idx, mode)) {
936 to_global.push_back(idx);
937 }
938 }
939 if (auto built {try_build_mode_dfa(std::move(to_global), mode)}) {
940 per_mode_dfa_[mode] = std::make_shared<const mode_dfa>(std::move(*built));
941 }
942 }
943 }
944
946 struct dispatch
947 {
948 std::array<std::vector<std::size_t>, 256> first_byte_index;
949 std::vector<std::size_t> general;
950 };
951
952 std::vector<rule> rules_;
955 std::vector<std::string> mode_names_;
956 std::map<std::string, std::size_t> mode_id_;
957 std::vector<dispatch> per_mode_;
958 std::vector<std::shared_ptr<const mode_dfa>> per_mode_dfa_;
959 std::vector<bool> mode_significant_;
960 };
961
970 {
971 public:
972
973 using iterator_category = std::input_iterator_tag;
975 using difference_type = std::ptrdiff_t;
976 using pointer = const token*;
977 using reference = const token&;
978
980 token_iterator() = default;
981
988 token_iterator(const lexer& owner,
989 std::string_view source,
990 eof_policy policy)
991 : owner_(&owner),
992 source_(source),
993 policy_(policy),
994 done_(false)
995 {
996 advance();
997 }
998
1001 {
1002 return current_;
1003 }
1004
1007 {
1008 return &current_;
1009 }
1010
1013 {
1014 advance();
1015 return *this;
1016 }
1017
1019 void operator++(int)
1020 {
1021 advance();
1022 }
1023
1029 [[nodiscard]] bool operator==(const token_iterator& other) const
1030 {
1031 return done_ == other.done_ && (done_ || cursor_.offset == other.cursor_.offset);
1032 }
1033
1039 [[nodiscard]] bool operator!=(const token_iterator& other) const
1040 {
1041 return !(*this == other);
1042 }
1043
1044 private:
1045
1046 const lexer* owner_ {nullptr};
1047 std::string_view source_;
1048 position cursor_ {0, 1, 1};
1049 std::vector<frame> stack_ {frame {.mode_id = 0, .entry_pos = position {0, 1, 1}}};
1052 bool eof_done_ {false};
1053 bool done_ {true};
1054
1056 void advance()
1057 {
1058 if (done_) {
1059 return;
1060 }
1062 return;
1063 }
1064 // Input exhausted: yield one end-of-input token if requested, else stop.
1067 eof_done_ = true;
1068 return;
1069 }
1070 done_ = true;
1071 }
1072 };
1073
1080 {
1081 public:
1082
1089 token_range(const lexer& owner,
1090 std::string_view source,
1091 eof_policy policy)
1092 : owner_(&owner),
1093 source_(source),
1094 policy_(policy)
1095 {}
1096
1098 [[nodiscard]] token_iterator begin() const
1099 {
1101 }
1102
1104 [[nodiscard]] token_iterator end() const
1105 {
1106 return token_iterator();
1107 }
1108
1109 private:
1110
1111 const lexer* owner_ {nullptr};
1112 std::string_view source_;
1114 };
1115
1116 inline token_range lexer::scan(std::string_view source,
1117 eof_policy policy) const&
1118 {
1119 return token_range(*this, source, policy);
1120 }
1121} // namespace scilex
1122
1123#endif // SCILEX_LEXER_HPP
Thrown when no rule matches at a position (a lexical error).
Definition lexer.hpp:123
lex_error(const std::string &message, position where)
Builds the error.
Definition lexer.hpp:131
position where() const noexcept
Returns the position of the unmatched byte.
Definition lexer.hpp:140
position where_
Where tokenization failed.
Definition lexer.hpp:147
A lexer built from an ordered list of rules.
Definition lexer.hpp:235
void validate_transitions() const
Fail-fast transition checks: a transition rule must consume input, and a push/set target must be a de...
Definition lexer.hpp:510
void add_to_mode(std::size_t m, std::size_t idx)
Adds rule idx to mode m's dispatch via REAL's exact first-byte API — the same 3-way split (nullable →...
Definition lexer.hpp:478
std::vector< dispatch > per_mode_
Dispatch index, one per mode (by id).
Definition lexer.hpp:957
column_unit columns() const noexcept
The unit this lexer counts position::column in (positions do not carry it, so a consumer that needs t...
Definition lexer.hpp:278
void build_dispatch()
Builds the per-mode first-byte dispatch from rules_ (once, at construction). "default" is mode 0; eve...
Definition lexer.hpp:535
scilex::column_unit columns_
The unit position::column is counted in.
Definition lexer.hpp:954
std::vector< std::string > audit_probes(const std::vector< std::size_t > &to_global) const
The bounded, deterministic probe inputs for the audit: every active rule's possible first bytes ∪ str...
Definition lexer.hpp:811
std::vector< rule > rules_
The ordered token rules.
Definition lexer.hpp:952
std::vector< bool > mode_significant_
Layout policy (empty = all significant).
Definition lexer.hpp:959
std::optional< mode_dfa > try_build_mode_dfa(std::vector< std::size_t > to_global, std::size_t mode)
Builds the mode_dfa for one mode, or std::nullopt if the mode cannot take the DFA fast path....
Definition lexer.hpp:896
static std::size_t valid_utf8_len(std::string_view s, std::size_t off)
The length (1–4) of a valid UTF-8 codepoint starting at off in s, or 0 when the byte there is not a v...
Definition lexer.hpp:675
bool mode_is_empty(std::size_t m) const
Whether mode m has no active rule (so nothing can match in it).
Definition lexer.hpp:499
bool starts_a_match(std::size_t mode, std::string_view source, std::size_t offset) const
Does a rule in mode match at offset in source? The error-recovery loop's stop test — the smallest suc...
Definition lexer.hpp:642
munch_result munch_at(std::size_t mode, std::string_view rest, unsigned char lead) const
The winning munch in mode at the start of rest (lead is rest's first byte), dispatching to the mode's...
Definition lexer.hpp:610
const std::vector< bool > & mode_significant() const noexcept
The per-mode-id layout-significance policy (see scilex::layout). Index by a token's mode_id; false ma...
Definition lexer.hpp:338
const std::string & mode_name(std::size_t id) const noexcept
The name of mode id (0 is "default"), for labelling tokens.
Definition lexer.hpp:344
static std::string position_label(position where)
Formats a position as "line:column" for diagnostics.
Definition lexer.hpp:372
bool audit_passes(const real::dfa &candidate, const std::vector< std::size_t > &to_global, std::size_t mode) const
The candidate DFA must reproduce the Pike munch on every probe: catches divergences the bytecode cann...
Definition lexer.hpp:863
void build_dfa_modes(const std::vector< std::string > &dfa_modes)
Opts the named dfa_modes into the DFA fast path (called once, after build_dispatch)....
Definition lexer.hpp:924
std::size_t intern_mode(const std::string &name)
Interns a mode name to its id, assigning the next id on first sight.
Definition lexer.hpp:466
void build_significance(const std::vector< std::string > &insignificant_modes)
Builds the layout-significance policy from the insignificant-mode names (validated against the intern...
Definition lexer.hpp:575
bool rule_active_in_mode(std::size_t idx, std::size_t mode) const
Whether rule idx is active in mode mode (mirrors build_dispatch, an empty in_mode is the default mode...
Definition lexer.hpp:792
bool scan_next(std::string_view source, position &cursor, std::vector< frame > &stack, token &out) const
Advances cursor to and past the next non-skipped token in the active mode, applying the winning rule'...
Definition lexer.hpp:396
std::map< std::string, std::size_t > mode_id_
Mode name -> id.
Definition lexer.hpp:956
std::vector< token > tokenize(std::string_view source, eof_policy policy=eof_policy::omit) const
Tokenizes source into the sequence of non-skipped tokens.
Definition lexer.hpp:298
token_range scan(std::string_view source, eof_policy policy=eof_policy::omit) const &&=delete
Deleted: the range would point into a temporary lexer.
munch_result pike_munch_in_mode(std::size_t mode, std::string_view rest, unsigned char lead) const
The per-rule Pike + first-byte-dispatch munch in mode at the start of rest (lead is rest's first byte...
Definition lexer.hpp:752
lexer(std::vector< rule > rules, std::vector< std::string > insignificant_modes={}, std::vector< std::string > dfa_modes={}, error_policy errors=error_policy::raise, column_unit columns=column_unit::bytes)
Builds a lexer from rules (taken by value, then moved in).
Definition lexer.hpp:262
static std::size_t column_step(std::string_view source, std::size_t off, scilex::column_unit unit)
How much the column advances when the byte at off in source is consumed, under unit....
Definition lexer.hpp:722
void advance(std::string_view source, position &cursor, std::size_t n) const
Advances cursor by n bytes of source, maintaining the 1-based line/column tracker (a newline resets t...
Definition lexer.hpp:656
std::vector< std::shared_ptr< const mode_dfa > > per_mode_dfa_
Per-mode DFA fast path (nullptr = Pike).
Definition lexer.hpp:958
std::vector< std::string > dfa_modes_active() const
The modes actually accelerated by a DFA fast path.
Definition lexer.hpp:356
error_policy errors_
What to do at an unmatched byte.
Definition lexer.hpp:953
token_range scan(std::string_view source, eof_policy policy=eof_policy::omit) const &
Returns a lazy range over the non-skipped tokens of source.
Definition lexer.hpp:1116
std::vector< std::string > mode_names_
Mode id -> name ("default" is id 0).
Definition lexer.hpp:955
bool may_start(std::size_t mode, unsigned char byte) const
O(1) pre-filter for error recovery: can a fixed-lead rule in mode begin with byte?...
Definition lexer.hpp:632
Forward (single-pass) iterator yielding one token at a time.
Definition lexer.hpp:970
token_iterator(const lexer &owner, std::string_view source, eof_policy policy)
Constructs a begin iterator over source for owner.
Definition lexer.hpp:988
std::ptrdiff_t difference_type
Required typedef.
Definition lexer.hpp:975
void operator++(int)
Post-increment (single-pass: no useful copy is returned).
Definition lexer.hpp:1019
std::vector< frame > stack_
Mode stack (top = active).
Definition lexer.hpp:1049
reference operator*() const
Returns the current token.
Definition lexer.hpp:1000
token current_
The current token.
Definition lexer.hpp:1050
bool operator==(const token_iterator &other) const
Equality: both exhausted, or both at the same offset.
Definition lexer.hpp:1029
pointer operator->() const
Member access to the current token.
Definition lexer.hpp:1006
position cursor_
Current scan position.
Definition lexer.hpp:1048
bool done_
True once exhausted (end sentinel).
Definition lexer.hpp:1053
const lexer * owner_
Rules provider (not owned).
Definition lexer.hpp:1046
std::string_view source_
Text being scanned.
Definition lexer.hpp:1047
token_iterator & operator++()
Advances to the next token.
Definition lexer.hpp:1012
token_iterator()=default
Constructs the end sentinel.
void advance()
Produces the next token, or marks the iterator exhausted.
Definition lexer.hpp:1056
bool operator!=(const token_iterator &other) const
Inequality.
Definition lexer.hpp:1039
std::input_iterator_tag iterator_category
Single-pass.
Definition lexer.hpp:973
bool eof_done_
End-of-input token already yielded.
Definition lexer.hpp:1052
eof_policy policy_
End-of-input policy.
Definition lexer.hpp:1051
A lazy range of tokens, returned by lexer::scan.
Definition lexer.hpp:1080
token_iterator end() const
End sentinel.
Definition lexer.hpp:1104
std::string_view source_
Text being scanned.
Definition lexer.hpp:1112
token_iterator begin() const
Begin iterator (produces the first token).
Definition lexer.hpp:1098
const lexer * owner_
Rules provider (not owned).
Definition lexer.hpp:1111
token_range(const lexer &owner, std::string_view source, eof_policy policy)
Builds the range.
Definition lexer.hpp:1089
eof_policy policy_
End-of-input policy.
Definition lexer.hpp:1113
The SciLex public API (scilex::lexer, scilex::rule, scilex::token).
Definition layout.hpp:47
void apply_transition(const rule &r, position start, std::vector< frame > &stack)
Applies rule r's mode transition (if any) to stack — the per-scan mode-stack mutation,...
Definition lexer.hpp:173
constexpr int error
Reserved token kind for a lexical-error run under scilex::error_policy::token.
Definition token.hpp:37
column_unit
The unit a token's position::column is counted in.
Definition lexer.hpp:221
@ codepoints
One column per Unicode scalar value (a valid UTF-8 codepoint).
@ bytes
One column per byte (the default; column == byte offset within the line + 1).
@ utf16
One column per UTF-16 code unit (BMP = 1, astral = 2) — the LSP unit.
eof_policy
Whether tokenization appends a synthetic end-of-input token.
Definition lexer.hpp:53
@ append
Append one end_of_input token at the end position.
@ omit
Stop at the last real token (default).
constexpr int end_of_input
Reserved token kind for the synthetic end-of-input token.
Definition token.hpp:26
error_policy
What a lexer does when it reaches a byte that no rule in the active mode can begin.
Definition lexer.hpp:200
One entry on the per-scan mode stack: the active mode and where it was entered (the entry position fe...
Definition lexer.hpp:155
std::size_t mode_id
Id of the active mode.
Definition lexer.hpp:156
position entry_pos
Where this mode was entered.
Definition lexer.hpp:157
Per-mode dispatch index: the first-byte buckets scoped to one mode.
Definition lexer.hpp:947
std::vector< std::size_t > general
Nullable rules (tried everywhere).
Definition lexer.hpp:949
std::array< std::vector< std::size_t >, 256 > first_byte_index
Rule indices by leading byte.
Definition lexer.hpp:948
An adopted per-mode DFA: the automaton plus its local→global rule map.
Definition lexer.hpp:592
std::vector< std::size_t > to_global
DFA local rule index -> global rules_ index.
Definition lexer.hpp:594
real::dfa dfa
Recognizes the mode's rules in one pass.
Definition lexer.hpp:593
A munch decision: whether a rule matched, which (global index), how many bytes — the small value scan...
Definition lexer.hpp:600
A mode transition, fired when its rule wins, acting on the scan's mode stack: enter a nested mode,...
Definition lexer.hpp:63
std::string target
The mode push/set enters; ignored (and omittable) for pop.
Definition lexer.hpp:73
op operation
Which transition to perform.
Definition lexer.hpp:72
std::size_t target_id
The interned id of target, resolved once when the lexer is built (see scilex::lexer::build_dispatch) ...
Definition lexer.hpp:79
op
The kind of transition.
Definition lexer.hpp:66
@ push
Enter target, remembering the mode below it (a nested context).
@ pop
Leave the current mode, returning to the one beneath it.
@ set
Replace the current mode with target (stack depth unchanged).
A location in the source text.
Definition token.hpp:48
std::size_t offset
0-based byte offset from the start of the source.
Definition token.hpp:49
std::size_t column
1-based byte column within the line.
Definition token.hpp:51
std::size_t line
1-based line number.
Definition token.hpp:50
A token rule: a kind, the pattern that recognizes it, whether matches are discarded (whitespace,...
Definition lexer.hpp:109
int kind
Kind assigned to tokens this rule produces.
Definition lexer.hpp:110
std::optional< mode_action > action
Mode transition fired when this rule wins.
Definition lexer.hpp:114
bool skip
If true, matches are consumed but not emitted.
Definition lexer.hpp:112
real::regex pattern
The recognizer (a linear-time REAL regex; its flags are the author's — see above).
Definition lexer.hpp:111
std::vector< std::string > in_mode
Modes this rule is active in; empty ⇒ {"default"}.
Definition lexer.hpp:113
One lexical token: a typed slice of the source.
Definition token.hpp:58
int kind
Caller-defined token kind (e.g. an enum value).
Definition token.hpp:59
The token produced by the lexer and its source position.