src/syntax/lex.bend

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