FE 0.15.0
A C++23 toolkit for writing compiler/interpreter frontends.
Loading...
Searching...
No Matches
lexer.h
Go to the documentation of this file.
1#pragma once
2
3#include <cstddef>
4
5#include <string>
6#include <string_view>
7
8#include "fe/driver.h"
9#include "fe/loc.h"
10#include "fe/ring.h"
11#include "fe/src.h"
12#include "fe/utf8.h"
13
14namespace fe {
15
16/// The blueprint for a lexer with a buffer of @p K tokens to peek into the future (Lexer::ahead).
17/// You can "override" Lexer::next via CRTP (@p S is the child).
18/// The whole source has to sit in @p buf: a Pos is an index into it, so there is nothing left to
19/// keep track of - Lexer::next just hands out the byte range the code point it consumed occupied.
20/// @p S must provide somewhere to report to:
21/// ```
22/// class MyLexer : public fe::Lexer<K, MyLexer> {
23/// fe::Driver& driver(); ///< The default diagnostic below lands in its Driver::error.
24///
25/// friend fe::Lexer<K, MyLexer>; ///< Otherwise, this may be private.
26/// };
27/// ```
28/// Lexer::utf8_err and Lexer::char_err come with a default; declare either in @p S to word it differently.
29template<size_t K, class S>
30class Lexer {
31private:
32 S& self() { return *static_cast<S*>(this); }
33 const S& self() const { return *static_cast<const S*>(this); }
34
35public:
36 Lexer(std::string_view buf)
37 : Lexer(buf, nullptr) {}
38 Lexer(const Src& src)
39 : Lexer(src.buf(), &src) {}
40
41protected:
42 /// Delegate here to funnel both of the above into a single ctor of your own.
43 Lexer(std::string_view buf, const Src* src)
44 : buf_(buf)
45 , src_(src) {
46 if (buf_.starts_with(utf8::Bom)) cursor_ = utf8::Bom.size();
47 for (size_t i = 0; i != K; ++i)
48 ahead_[i] = decode();
49 start();
50 }
51
52 /// A decoded code point together with the byte range it occupies.
53 struct Ahead {
54 char32_t c = utf8::EoF;
56 };
57
58 char32_t ahead(size_t i = 0) const { return ahead_[i].c; }
59
60 /// Loc%ation of the next character to be consumed (Lexer::ahead()); empty once the buffer is exhausted.
61 Loc peek() const { return {src_, ahead_[0].begin, ahead_[0].end}; }
62
63 /// Invoke before assembling the next token.
64 void start() { loc_ = peek().anew_begin(); }
65
66 /// @name Text
67 /// What has been lexed since Lexer::start.
68 /// The whole source sits in Lexer::buf_, so Lexer::loc_ already *is* the token and Lexer::view costs nothing.
69 ///@{
70 std::string_view view() const { return buf_.substr(loc_.begin.off, loc_.size()); }
71
72 /// Lexer::view, case-folded - what a case-insensitive language like FORTRAN or SQL wants to intern.
73 /// @note Byte-wise, which is all it takes: only ASCII folds, and no UTF-8 sequence spells it.
74 std::string lower() const { return fold(fe::utf8::tolower); }
75 std::string upper() const { return fold(fe::utf8::toupper); }
76
77 // Transform view() via @p f.
78 std::string fold(char32_t (*f)(char32_t) noexcept) const {
79 std::string res(view());
80 for (auto& c : res)
81 c = (char)f((char32_t)(uint8_t)c);
82 return res;
83 }
84 ///@}
85
86 /// @name Accept
87 ///@{
88
89 /// Get next `char32_t` in Lexer::buf_ and extend Lexer::loc_ to cover it.
90 /// @returns utf8::Invalid on an invalid UTF-8 sequence.
91 char32_t next() {
92 loc_.end = ahead_[0].end;
93 return ahead_.put(decode()).c;
94 }
95
96 /// Accept next character in Lexer::buf_ and Lexer::next it, if @p pred holds.
97 bool accept(auto pred) {
98 if (!pred(ahead())) return false;
99 self().next();
100 return true;
101 }
102
103 // clang-format off
104 bool accept(char32_t c) { return accept([c](char32_t d) { return c == d; }); }
105 bool accept(char c) { return accept((char32_t)c); }
106 bool accept(char8_t c) { return accept((char32_t)c); }
107 // clang-format on
108
109 /// Lexer::next as long as @p pred holds.
110 /// An ASCII run is taken straight out of Lexer::buf_ - no code point is decoded and the lookahead
111 /// is re-primed once at the end, which is what makes scanning an identifier or a stretch of white
112 /// space cost a compare per byte.
113 /// A character beyond ASCII falls back to Lexer::next, so @p pred may match one.
114 /// @note Only worth it if the lexed text is expected to be long such as identifiers or comments.
115 /// @returns the run just consumed.
116 std::string_view accept_while(auto pred) {
117 auto begin = ahead_[0].begin.off;
118
119 while (true) {
120 auto run = ahead_[0].begin.off;
121 for (; run != buf_.size() && (uint8_t)buf_[run] < 0x80 && pred((char32_t)(uint8_t)buf_[run]); ++run) {}
122
123 if (run != ahead_[0].begin.off) {
124 loc_.end = Pos((uint32_t)run);
125 cursor_ = run;
126 for (size_t i = 0; i != K; ++i)
127 ahead_.put(decode());
128 }
129
130 auto c = ahead();
131 if (c < 0x80 || c == utf8::EoF || !pred(c)) break;
132 self().next();
133 }
134
135 return buf_.substr(begin, loc_.end.off - begin);
136 }
137
138 /// Lexer::next up to - but not including - the next byte that is @p a or @p b.
139 /// The stop bytes are a *set*: whichever comes first ends the run, and this is no substring search.
140 /// Stops at the end of Lexer::buf_ if none of them ever shows up.
141 /// No UTF-8 sequence spells an ASCII byte, so such a run needs no decoding at all:
142 /// this is how to skip a comment or a string literal,
143 /// where Lexer::accept_while pays a compare - and Lexer::next a whole utf8::decode - per character.
144 /// @note Nothing in the run is validated as UTF-8 - malformed bytes cannot spell @p a or @p b either.
145 /// @returns the run just consumed.
146 std::string_view accept_while_none_of(char8_t a, char8_t b) {
147 assert(a < 0x80 && b < 0x80 && "only an ASCII byte can be searched for without decoding");
148 auto begin = ahead_[0].begin.off;
149 auto run = begin;
150
151 for (auto e = buf_.size(); run != e; ++run)
152 if (auto c = (char8_t)buf_[run]; c == a || c == b) break;
153
154 return skip_to(begin, run);
155 }
156
157 /// As Lexer::accept_while_none_of(char8_t, char8_t), but a single stop byte, which one `memchr` finds outright.
158 std::string_view accept_while_none_of(char8_t a) {
159 assert(a < 0x80 && "only an ASCII byte can be searched for without decoding");
160 auto begin = ahead_[0].begin.off;
161 auto pos = buf_.find((char)a, begin);
162 return skip_to(begin, pos == std::string_view::npos ? buf_.size() : pos);
163 }
164
165 std::string_view accept_while_none_of(char a) { return accept_while_none_of((char8_t)a); }
166 std::string_view accept_while_none_of(char a, char b) { return accept_while_none_of((char8_t)a, (char8_t)b); }
167
168 /// Lexer::next up to - but not including - the next occurrence of @p seq.
169 /// Where Lexer::accept_while_none_of takes a set of bytes, this one takes a *sequence*:
170 /// only @p seq, spelled in exactly that order, ends the run - which is what closes a `/*` comment.
171 /// Stops at the end of Lexer::buf_ if @p seq never shows up.
172 /// @note Nothing in the run is decoded or validated as UTF-8.
173 /// @returns the run just consumed.
174 std::string_view accept_until(std::string_view seq) {
175 assert(!seq.empty() && "an empty sequence matches at once, so the lexer would not advance");
176 auto begin = ahead_[0].begin.off;
177 auto pos = buf_.find(seq, begin);
178 return skip_to(begin, pos == std::string_view::npos ? buf_.size() : pos);
179 }
180 ///@}
181
182 /// @name Recover
183 /// Lexer::next input that cannot be part of a token, report it, and keep the current lexer going.
184 /// Invoke after Lexer::start, so Lexer::loc_ spans exactly what was discarded.
185 ///@{
186 /// A whole run of malformed UTF-8, if any, reported as one `S::utf8_err`.
187 /// Check this *before* your token dispatch: utf8::Invalid is no code point and matches no rule of yours.
189 if (!accept(utf8::Invalid)) return false;
190 while (accept(utf8::Invalid)) {}
191 self().utf8_err();
192 return true;
193 }
194
195 /// One character, reported as `S::char_err`.
196 /// This is the last resort of your token dispatch: nothing in your language starts with it.
197 /// @warning Never at utf8::EoF - accept that first or your lexer will spin.
199 auto c = ahead();
200 self().next();
201 self().char_err(c);
202 }
203 ///@}
204
205 /// @name Diagnostics
206 /// The defaults @p S may replace with one of its own.
207 /// Each yields the Error it reported into, so a Note can be chained.
208 ///@{
209 fe::Error& error() { return self().driver().error(); }
210 const fe::Error& error() const { return self().driver().error(); }
211
212 /// Lexer::recover_utf8 discarded the malformed bytes at Lexer::loc_.
214 static_assert(
215 requires(S& s) { s.driver(); },
216 "provide `fe::Driver& driver()` in your lexer - or a `utf8_err` of your own");
217 return error().e(loc_, "invalid UTF-8 sequence");
218 }
219
220 /// Lexer::recover_char discarded @p c at Lexer::loc_.
221 fe::Error& char_err(char32_t c) {
222 static_assert(
223 requires(S& s) { s.driver(); },
224 "provide `fe::Driver& driver()` in your lexer - or a `char_err` of your own");
225 return error().e(loc_, "invalid input character `{}`", utf8::Char32(c));
226 }
227 ///@}
228
229 std::string_view buf_;
230 const Src* src_;
231 size_t cursor_ = 0; ///< Byte offset of the first not yet decoded character.
233 Loc loc_; ///< Loc%ation of the token we are currently constructing - see Lexer::view.
234
235private:
236 /// Consume Lexer::buf_ up to byte offset @p run, without decoding a thing.
237 /// @returns that range.
238 std::string_view skip_to(size_t begin, size_t run) {
239 if (run != begin) {
240 loc_.end = Pos((uint32_t)run);
241 cursor_ = run;
242 for (size_t i = 0; i != K; ++i)
243 ahead_.put(decode());
244 }
245
246 return buf_.substr(begin, run - begin);
247 }
248
249 Ahead decode() {
250 auto begin = cursor_;
251 auto c = utf8::decode(buf_, cursor_);
252 return {c, Pos((uint32_t)begin), Pos((uint32_t)cursor_)};
253 }
254};
255
256} // namespace fe
Collects diagnostics and hands each to the Diag that lays it out.
Definition error.h:23
Error & e(Loc loc, cite_string< Args... > s, Args &&... args)
Definition error.h:96
Lexer(const Src &src)
Definition lexer.h:38
char32_t next()
Get next char32_t in Lexer::buf_ and extend Lexer::loc_ to cover it.
Definition lexer.h:91
void start()
Invoke before assembling the next token.
Definition lexer.h:64
bool recover_utf8()
Definition lexer.h:188
std::string_view accept_while_none_of(char8_t a)
As Lexer::accept_while_none_of(char8_t, char8_t), but a single stop byte, which one memchr finds outr...
Definition lexer.h:158
Loc loc_
Location of the token we are currently constructing - see Lexer::view.
Definition lexer.h:233
Lexer(std::string_view buf)
Definition lexer.h:36
std::string_view accept_until(std::string_view seq)
Lexer::next up to - but not including - the next occurrence of seq.
Definition lexer.h:174
Loc peek() const
Location of the next character to be consumed (Lexer::ahead()); empty once the buffer is exhausted.
Definition lexer.h:61
size_t cursor_
Byte offset of the first not yet decoded character.
Definition lexer.h:231
char32_t c
Definition lexer.h:54
bool accept(auto pred)
Accept next character in Lexer::buf_ and Lexer::next it, if pred holds.
Definition lexer.h:97
const fe::Error & error() const
Definition lexer.h:210
char32_t ahead(size_t i=0) const
Definition lexer.h:58
Ring< Ahead, K > ahead_
Definition lexer.h:232
std::string_view accept_while_none_of(char8_t a, char8_t b)
Lexer::next up to - but not including - the next byte that is a or b.
Definition lexer.h:146
std::string_view view() const
Definition lexer.h:70
std::string lower() const
Lexer::view, case-folded - what a case-insensitive language like FORTRAN or SQL wants to intern.
Definition lexer.h:74
fe::Error & char_err(char32_t c)
Lexer::recover_char discarded c at Lexer::loc_.
Definition lexer.h:221
fe::Error & error()
Definition lexer.h:209
bool accept(char c)
Definition lexer.h:105
const Src * src_
Definition lexer.h:230
Lexer(std::string_view buf, const Src *src)
Delegate here to funnel both of the above into a single ctor of your own.
Definition lexer.h:43
void recover_char()
One character, reported as S::char_err.
Definition lexer.h:198
fe::Error & utf8_err()
Lexer::recover_utf8 discarded the malformed bytes at Lexer::loc_.
Definition lexer.h:213
bool accept(char8_t c)
Definition lexer.h:106
std::string_view accept_while_none_of(char a)
Definition lexer.h:165
bool accept(char32_t c)
Definition lexer.h:104
std::string fold(char32_t(*f)(char32_t) noexcept) const
Definition lexer.h:78
std::string_view buf_
Definition lexer.h:229
std::string upper() const
Definition lexer.h:75
std::string_view accept_while(auto pred)
Lexer::next as long as pred holds.
Definition lexer.h:116
std::string_view accept_while_none_of(char a, char b)
Definition lexer.h:166
A decoded code point together with the byte range it occupies.
Definition lexer.h:53
A ring buffer with N elements.
Definition ring.h:15
T put(T item)
Puts item into buffer.
Definition ring.h:52
The content of one source file together with the offsets its rows start at.
Definition src.h:40
char32_t decode(std::istream &is)
Decodes the next UTF-8 sequence from is into a single char32_t.
Definition utf8.h:64
constexpr char32_t toupper(char32_t c) noexcept
Definition utf8.h:156
constexpr char32_t tolower(char32_t c) noexcept
Definition utf8.h:155
Definition algo.h:17
Location within a Src: the half-open byte range [Loc::begin, Loc::end).
Definition loc.h:42
Pos begin
Definition loc.h:92
constexpr Loc anew_begin() const noexcept
Definition loc.h:63
Pos end
It's called end because - just like an STL iterator - it refers to the byte one past the last one wit...
Definition loc.h:93
constexpr uint32_t size() const noexcept
Definition loc.h:65
Byte offset into a Src; pass around as value.
Definition loc.h:16
uint32_t off
Definition loc.h:31
constexpr Pos() noexcept=default
Creates an invalid Position.
Wrapper for char32_t with an operator<< that writes UTF-8.
Definition utf8.h:125