src/syntax/lex.bend
bolt@1.21.0.0 · 380 lines · 0 definitions
1# src/syntax/lex: a source as tokens with positions. Lossless: every char lands
2# in exactly one token, so the texts concatenate back to the source, whatever
3# the source is. A string runs to its closing quote across newlines, as in
4# bend, so an unclosed one runs to the end of the file (bend rejects that
5# file). A char literal and a comment end at their line's end. One char per
6# step, a tail-recursive state machine.
7#
8# A name is letters, digits, `_` and `.` (`List.map`, `M.go`). Operator chars
9# group into runs (`->`, `<-`, `=>`, `==`, `++`), except `:`, which always
10# stands alone: `List<U32>:` must not read `>:`.
11#
12# Kinds tell apart what binds from what does not, so a walk over the tokens
13# can branch by constructor: a keyword (TKey), a dotted name (TDotted: never
14# a binder), a capitalized one (TUpper: a constructor before `{`, a type, or
15# a binder), `_` (TWild), and the operators that bind: `:` `=` `<-` `->` `=>`
16# `@` `&`.
17import Base
18import bolt@1.21.0.0/src/rchars.bend as Rchars
19import bolt@1.21.0.0/src/lazy/lazy.bend as Lazy
20
21# the kinds; TName is a plain name (lowercase, undotted): what a pattern can
22# bind
23type TokKind is Data:
24 TName{}
25 TUpper{}
26 TDotted{}
27 TWild{}
28 TKey{}
29 TNum{}
30 TStr{}
31 TChar{}
32 TComment{}
33 TSpace{}
34 TNewline{}
35 TOp{}
36 TColon{}
37 TEq{}
38 TBind{}
39 TArrow{}
40 TLam{}
41 TAll{}
42 TAmp{}
43 TOpen{}
44 TClose{}
45 TComma{}
46
47# line and col are 0-based, of the token's first char
48type Tok is Data:
49 Tok{kind: TokKind, text: String, line: U32, col: U32}
50
51# what a char is, for the machine
52type Class is Data:
53 CAlpha{}
54 CDigit{}
55 CDot{}
56 CQuote{}
57 CTick{}
58 CBack{}
59 CHash{}
60 CSpace{}
61 CNewline{}
62 COp{}
63 CColon{}
64 COpen{}
65 CClose{}
66 CComma{}
67
68# what the machine is inside of; Fresh: the next char starts a token
69type Mode is Data:
70 Fresh{}
71 InName{}
72 InNum{}
73 InStr{}
74 InStrEsc{}
75 InChar{}
76 InCharEsc{}
77 InComment{}
78 InSpace{}
79 InOp{}
80
81# buf and toks are reversed; (sl, sc) is where the current token started
82type St is Data:
83 St{mode: Mode, kind: TokKind, line: U32, col: U32, sl: U32, sc: U32, buf: List<&2, Char>, toks: List<&2, Tok>}
84
85# the class of a code point, given the class any other character would get.
86# Written as comparisons rather than as `case '.':` arms: a character literal
87# in a pattern is a U32 literal inside a constructor pattern, and bend's C
88# backend pays about 90 MB for each one -- these eighteen cost 1.57 GB as
89# literal arms and 0.10 GB written this way.
90def classify.go(+xx: U32, other: Class) -> Class:
91 Bool.pick(Class, U32.is_eq(xx, 46), CDot{}, # '.'
92 Bool.pick(Class, U32.is_eq(xx, 34), CQuote{}, # '"'
93 Bool.pick(Class, U32.is_eq(xx, 39), CTick{}, # '\''
94 Bool.pick(Class, U32.is_eq(xx, 92), CBack{}, # '\\'
95 Bool.pick(Class, U32.is_eq(xx, 35), CHash{}, # '#'
96 Bool.pick(Class, U32.is_eq(xx, 32), CSpace{}, # ' '
97 Bool.pick(Class, U32.is_eq(xx, 9), CSpace{}, # '\t'
98 Bool.pick(Class, U32.is_eq(xx, 13), CSpace{}, # '\r'
99 Bool.pick(Class, U32.is_eq(xx, 10), CNewline{}, # '\n'
100 Bool.pick(Class, U32.is_eq(xx, 58), CColon{}, # ':'
101 Bool.pick(Class, U32.is_eq(xx, 44), CComma{}, # ','
102 Bool.pick(Class, U32.is_eq(xx, 40), COpen{}, # '('
103 Bool.pick(Class, U32.is_eq(xx, 91), COpen{}, # '['
104 Bool.pick(Class, U32.is_eq(xx, 123), COpen{}, # '{'
105 Bool.pick(Class, U32.is_eq(xx, 41), CClose{}, # ')'
106 Bool.pick(Class, U32.is_eq(xx, 93), CClose{}, # ']'
107 Bool.pick(Class, U32.is_eq(xx, 125), CClose{}, # '}'
108 Bool.pick(Class, U32.is_eq(xx, 95), CAlpha{}, # '_'
109 other))))))))))))))))))
110
111# a char's class
112def classify(+cc: Char) -> Class:
113 +other = Bool.pick(Class, Char.is_alpha(cc), CAlpha{},
114 Bool.pick(Class, Char.is_digit(cc), CDigit{}, COp{}))
115 classify.go(Char.to_u32(cc), other)
116
117# does a char of this class continue the token the machine is inside of?
118def continues(mm: Mode, cls: Class) -> Bool:
119 match mm cls:
120 case Fresh{} c:
121 False{}
122 case InName{} CAlpha{}:
123 True{}
124 case InName{} CDigit{}:
125 True{}
126 case InName{} CDot{}:
127 True{}
128 case InNum{} CDigit{}:
129 True{}
130 case InNum{} CDot{}:
131 True{}
132 case InNum{} CAlpha{}:
133 True{}
134 case InStr{} c:
135 True{}
136 case InStrEsc{} c:
137 True{}
138 case InChar{} CNewline{}:
139 False{}
140 case InChar{} c:
141 True{}
142 case InCharEsc{} CNewline{}:
143 False{}
144 case InCharEsc{} c:
145 True{}
146 case InComment{} CNewline{}:
147 False{}
148 case InComment{} c:
149 True{}
150 case InSpace{} CSpace{}:
151 True{}
152 case InOp{} COp{}:
153 True{}
154 case InOp{} CDot{}:
155 True{}
156 case m c:
157 False{}
158
159# the mode after a char that continued the token
160def after(mm: Mode, cls: Class) -> Mode:
161 match mm cls:
162 case InStr{} CQuote{}:
163 Fresh{}
164 case InStr{} CBack{}:
165 InStrEsc{}
166 case InStrEsc{} c:
167 InStr{}
168 case InChar{} CTick{}:
169 Fresh{}
170 case InChar{} CBack{}:
171 InCharEsc{}
172 case InCharEsc{} c:
173 InChar{}
174 case m c:
175 m
176
177# the mode and the kind of the token a char starts
178def begin.mode(cls: Class) -> Mode:
179 match cls:
180 case CAlpha{}:
181 InName{}
182 case CDigit{}:
183 InNum{}
184 case CQuote{}:
185 InStr{}
186 case CTick{}:
187 InChar{}
188 case CHash{}:
189 InComment{}
190 case CSpace{}:
191 InSpace{}
192 case COp{}:
193 InOp{}
194 case CDot{}:
195 InOp{}
196 case CBack{}:
197 InOp{}
198 case other:
199 Fresh{}
200
201def begin.kind(cls: Class) -> TokKind:
202 match cls:
203 case CAlpha{}:
204 TName{}
205 case CDigit{}:
206 TNum{}
207 case CQuote{}:
208 TStr{}
209 case CTick{}:
210 TChar{}
211 case CHash{}:
212 TComment{}
213 case CSpace{}:
214 TSpace{}
215 case CNewline{}:
216 TNewline{}
217 case COpen{}:
218 TOpen{}
219 case CClose{}:
220 TClose{}
221 case CComma{}:
222 TComma{}
223 case other:
224 TOp{}
225
226# the buffered chars as a string, in order
227def word(buf: List<&2, Char>) -> String:
228 Rchars.text(buf)
229
230# is the text one of Bend's keywords?
231def is_keyword(+tt: String) -> Bool:
232 List.contains(~String, ~String.eq,
233 ["def", "type", "law", "match", "case", "do", "return", "for", "exs", "where", "is", "import",
234 "Type", "Data", "Kind", "Quant"], tt)
235
236# does the text start with a capital?
237def upper_first(cs: List<&2, Char>) -> Bool:
238 match cs:
239 case Nil{}:
240 False{}
241 case Con{c, t}:
242 Char.is_upper(c)
243
244# a whole name's kind
245def refine_name(+tt: String) -> TokKind:
246 Bool.pick(TokKind, is_keyword(tt), TKey{},
247 Bool.pick(TokKind, String.contains(tt, "."), TDotted{},
248 Bool.pick(TokKind, String.eq(tt, "_"), TWild{},
249 Bool.pick(TokKind, upper_first(String.to_list(tt)), TUpper{}, TName{}))))
250
251# a whole operator's kind
252def refine_op(+tt: String) -> TokKind:
253 Bool.pick(TokKind, String.eq(tt, ":"), TColon{},
254 Bool.pick(TokKind, String.eq(tt, "="), TEq{},
255 Bool.pick(TokKind, String.eq(tt, "<-"), TBind{},
256 Bool.pick(TokKind, String.eq(tt, "->"), TArrow{},
257 Bool.pick(TokKind, String.eq(tt, "=>"), TLam{},
258 Bool.pick(TokKind, Bool.or(String.eq(tt, "@"), Bool.or(String.eq(tt, "@+"), String.eq(tt, "@-"))), TAll{},
259 Bool.pick(TokKind, String.eq(tt, "&"), TAmp{}, TOp{})))))))
260
261# a name's or an operator's kind, once its text is whole
262def refine(kind: TokKind, tt: String) -> TokKind:
263 match kind:
264 case TName{}:
265 refine_name(tt)
266 case TOp{}:
267 refine_op(tt)
268 case other:
269 other
270
271# the token so far joins the others
272def flush(kind: TokKind, sl: U32, sc: U32, +buf: List<&2, Char>, +toks: List<&2, Tok>) -> List<&2, Tok>:
273 +t = word(buf)
274 Bool.pick(List<&2, Tok>, List.is_empty(&2, Char, buf), toks, Tok{refine(kind, t), t, sl, sc} <> toks)
275
276# is the char a line break, '\n'? The one char whose class is CNewline
277def is_newline(cc: Char) -> Bool:
278 U32.is_eq(Char.to_u32(cc), 10)
279
280# one char: it continues the current token, or starts a new one. Only the
281# branch taken is built: the token is flushed (its text reversed, its kind
282# refined) once, where it ends, not on every char of it
283def step(+cc: Char, st: St) -> St:
284 St{+mode, +kind, +line, +col, +sl, +sc, +buf, +toks} = st
285 +cls = classify(cc)
286 +nl = is_newline(cc)
287 +line2 = Bool.pick(U32, nl, (line + 1 : U32), line)
288 +col2 = Bool.pick(U32, nl, 0, (col + 1 : U32))
289 Lazy.either(St, continues(mode, cls),
290 _u => St{after(mode, cls), kind, line2, col2, sl, sc, cc <> buf, toks},
291 _v => St{begin.mode(cls), begin.kind(cls), line2, col2, line, col, [cc], flush(kind, sl, sc, buf, toks)})
292
293# every char, in order
294def run(cs: List<&2, Char>, st: St) -> St:
295 match cs:
296 case Nil{}:
297 st
298 case Con{c, t}:
299 run(t, step(c, st))
300
301# the tokens once the chars are over, the last one flushed
302def finish(st: St) -> List<&2, Tok>:
303 St{mode, kind, line, col, sl, sc, buf, toks} = st
304 List.reverse(&2, Tok, flush(kind, sl, sc, buf, toks))
305
306# a source as tokens
307def tokens(source: String) -> List<&2, Tok>:
308 finish(run(String.to_list(source), St{Fresh{}, TSpace{}, 0, 0, 0, 0, [], []}))
309
310# the tokens' texts, back to back: the source
311def text.go(toks: List<&2, Tok>, acc: List<&2, String>) -> List<&2, String>:
312 match toks:
313 case Nil{}:
314 acc
315 case Con{Tok{k, t, l, c}, rest}:
316 text.go(rest, t <> acc)
317
318# the tokens' texts, back to back: the source
319def text(toks: List<&2, Tok>) -> String:
320 String.concat(List.reverse(&2, String, text.go(toks, [])))
321
322# an opening bracket?
323def is_open(kk: TokKind) -> Bool:
324 match kk:
325 case TOpen{}:
326 True{}
327 case other:
328 False{}
329
330# a comma?
331def is_comma(kk: TokKind) -> Bool:
332 match kk:
333 case TComma{}:
334 True{}
335 case other:
336 False{}
337
338# any identifier: a name, a keyword, a dotted or a capitalized one, `_`
339def is_name(kk: TokKind) -> Bool:
340 match kk:
341 case TName{}:
342 True{}
343 case TUpper{}:
344 True{}
345 case TDotted{}:
346 True{}
347 case TWild{}:
348 True{}
349 case TKey{}:
350 True{}
351 case other:
352 False{}
353
354# a newline?
355def is_nl(kk: TokKind) -> Bool:
356 match kk:
357 case TNewline{}:
358 True{}
359 case other:
360 False{}
361
362# not space, a newline or a comment
363def significant(kk: TokKind) -> Bool:
364 match kk:
365 case TSpace{}:
366 False{}
367 case TNewline{}:
368 False{}
369 case TComment{}:
370 False{}
371 case other:
372 True{}
373
374# a string literal?
375def is_str(kk: TokKind) -> Bool:
376 match kk:
377 case TStr{}:
378 True{}
379 case other:
380 False{}