src/syntax/outline.bend
bolt@1.11.0.0 · 597 lines · 0 definitions
1# src/syntax/outline: a source's top-level items, read line by line. A top-level
2# item starts at column 0, so a line the outline does not understand costs
3# nothing: it is skipped, and the items around it still stand. That tolerance
4# is the point: an editor asks about files that are half-written.
5#
6# A line that starts inside a string literal (the lexer's, which runs across
7# newlines as bend's does) is text, not a line of code: it is never an item,
8# a comment or a blank, and it joins a pending header as one more line.
9import Base
10import bolt@1.11.0.0/src/lazy/lazy.bend as Lazy
11import bolt@1.11.0.0/src/syntax/lex.bend as Lex
12
13# what an item is; ILocal is not read from a source, it is how the server
14# offers a binder as a candidate
15type ItemKind is Data:
16 IImport{}
17 IDef{}
18 ILaw{}
19 IType{}
20 ICtor{}
21 ILocal{}
22
23# line is 0-based; sig is the header text (a def's through its `:`, a law's
24# whole block, a constructor's line); doc is the comment block right above;
25# path is an import's target
26type Item is Data:
27 Item{kind: ItemKind, name: String, line: U32, sig: String, doc: String, path: String}
28
29# what a line is, by its first chars
30type Cls is Data:
31 LBlank{}
32 LComment{text: String}
33 LIndent{text: String}
34 LImport{}
35 LDef{}
36 LLaw{}
37 LType{}
38 LOther{}
39
40# Pending: a def's header that has not reached its `:`, or a law's block
41type Mode is Data:
42 Top{}
43 InType{}
44 Pending{kind: ItemKind, name: String, line: U32, doc: String, sig: List<&2, String>}
45
46# doc and out are reversed
47type St is Data:
48 St{mode: Mode, n: U32, doc: List<&2, String>, out: List<&2, Item>}
49
50# the class when the prefix matches, else other
51def classify.kw(hit: Bool, yes: Cls) -> Cls:
52 match hit:
53 case True{}:
54 yes
55 case False{}:
56 LOther{}
57
58# leading spaces (reversed) in front of the rest of the line
59def classify.rebuild(acc: List<&2, Char>, rest: String) -> String:
60 match acc:
61 case Nil{}:
62 rest
63 case Con{h, t}:
64 classify.rebuild(t, SCon{h, rest})
65
66# an indented line when the first char is a space, else other
67def classify.white.hit(indent: Bool, acc: List<&2, Char>, hh: Char, rest: String) -> Cls:
68 match indent:
69 case True{}:
70 LIndent{classify.rebuild(acc, SCon{hh, rest})}
71 case False{}:
72 LOther{}
73
74# whitespace after a leading space-class char. `space` says `h` is whitespace;
75# `acc` is the whitespace before `h`, reversed; `indent` when that line opened
76# with a space
77def classify.white(tt: String, space: Bool, hh: Char, indent: Bool, acc: List<&2, Char>) -> Cls:
78 match tt space:
79 case SNil{} True{}:
80 LBlank{}
81 case SNil{} False{}:
82 classify.white.hit(indent, acc, hh, SNil{})
83 case SCon{+h2, r} True{}:
84 classify.white(r, Char.is_space(h2), h2, indent, hh <> acc)
85 case SCon{h2, r} False{}:
86 classify.white.hit(indent, acc, hh, SCon{h2, r})
87
88# a line whose first char is whitespace
89def classify.white.enter(tt: String, +xx: U32) -> Cls:
90 classify.white(tt, True{}, Char.from_u32(xx), U32.is_eq(xx, 32), []) # ' '
91
92# `type `, once the `t` is known
93def classify.ktype(hit: Bool, tt: String) -> Cls:
94 match hit:
95 case True{}:
96 classify.kw(String.starts_with(tt, "ype "), LType{})
97 case False{}:
98 LOther{}
99
100# `law `, else `type `
101def classify.klaw(hit: Bool, +xx: U32, tt: String) -> Cls:
102 match hit:
103 case True{}:
104 classify.kw(String.starts_with(tt, "aw "), LLaw{})
105 case False{}:
106 classify.ktype(U32.is_eq(xx, 116), tt) # 't'
107
108# `@unsafe def `, else `law `
109def classify.kat(hit: Bool, +xx: U32, tt: String) -> Cls:
110 match hit:
111 case True{}:
112 classify.kw(String.starts_with(tt, "unsafe def "), LDef{})
113 case False{}:
114 classify.klaw(U32.is_eq(xx, 108), xx, tt) # 'l'
115
116# `def `, else `@unsafe def `
117def classify.kdef(hit: Bool, +xx: U32, tt: String) -> Cls:
118 match hit:
119 case True{}:
120 classify.kw(String.starts_with(tt, "ef "), LDef{})
121 case False{}:
122 classify.kat(U32.is_eq(xx, 64), xx, tt) # '@'
123
124# `import `, else `def `
125def classify.kimp(hit: Bool, +xx: U32, tt: String) -> Cls:
126 match hit:
127 case True{}:
128 classify.kw(String.starts_with(tt, "mport "), LImport{})
129 case False{}:
130 classify.kdef(U32.is_eq(xx, 100), xx, tt) # 'd'
131
132# a column-0 keyword, from the first character's code
133def classify.keys(+xx: U32, tt: String) -> Cls:
134 classify.kimp(U32.is_eq(xx, 105), xx, tt) # 'i'
135
136# a column-0 keyword keeps the line; the class comes from its first character
137def classify.hold.keys(+xx: U32, cc: Char, +tt: String) -> Cls & String:
138 (classify.keys(xx, tt), SCon{cc, tt})
139
140# whitespace that is not a leading space keeps the line: the class may not
141def classify.hold.gap.keep(+xx: U32, cc: Char, +tt: String) -> Cls & String:
142 (classify.white.enter(tt, xx), SCon{cc, tt})
143
144# a leading space is blank or indented, and that class already carries the line
145def classify.hold.gap(indent: Bool, xx: U32, cc: Char, tt: String) -> Cls & String:
146 match indent:
147 case True{}:
148 (classify.white.enter(tt, xx), SNil{})
149 case False{}:
150 classify.hold.gap.keep(xx, cc, tt)
151
152# whitespace, else a keyword
153def classify.hold.space(space: Bool, +xx: U32, cc: Char, tt: String) -> Cls & String:
154 match space:
155 case True{}:
156 classify.hold.gap(U32.is_eq(xx, 32), xx, cc, tt) # ' '
157 case False{}:
158 classify.hold.keys(xx, cc, tt)
159
160# a comment keeps its text; anything else is whitespace or a keyword
161def classify.hold.hash(is_hash: Bool, space: Bool, xx: U32, cc: Char, tt: String) -> Cls & String:
162 match is_hash:
163 case True{}:
164 (LComment{SCon{cc, tt}}, SNil{})
165 case False{}:
166 classify.hold.space(space, xx, cc, tt)
167
168# the first character, then the rest of the line
169def classify.hold.open(+xx: U32, +cc: Char, tt: String) -> Cls & String:
170 classify.hold.hash(U32.is_eq(xx, 35), Char.is_space(cc), xx, cc, tt) # '#'
171
172# the class, and the line when the class does not already carry it
173def classify.hold(ss: String) -> Cls & String:
174 match ss:
175 case SNil{}:
176 (LBlank{}, SNil{})
177 case SCon{+c, t}:
178 classify.hold.open(Char.to_u32(c), c, t)
179
180# `(`, `<`, `:`, `{` or a space ends a name
181def name.stop(+hh: Char) -> Bool:
182 Bool.or(Char.is_eq(hh, '('),
183 Bool.or(Char.is_eq(hh, '<'),
184 Bool.or(Char.is_eq(hh, ':'),
185 Bool.or(Char.is_eq(hh, '{'), Char.is_eq(hh, ' ')))))
186
187# a delimiter is not part of the name
188def name.push(stop: Bool, hh: Char, acc: String) -> String:
189 match stop:
190 case True{}:
191 acc
192 case False{}:
193 SCon{hh, acc}
194
195# the name, reversed into acc, up to a delimiter
196def name.go(ss: String, acc: String, stop: Bool) -> String:
197 match ss stop:
198 case SNil{} _:
199 String.reverse(acc)
200 case SCon{h, t} True{}:
201 String.reverse(acc)
202 case SCon{+h, t} False{}:
203 name.go(t, name.push(name.stop(h), h, acc), name.stop(h))
204
205# the name that starts a header: up to `(`, `<`, `:`, `{` or a space
206def name(ss: String) -> String:
207 name.go(ss, SNil{}, False{})
208
209def lstrip.go(cs: List<&2, Char>) -> List<&2, Char>:
210 match cs:
211 case Con{' ', t}:
212 lstrip.go(t)
213 case other:
214 other
215
216# leading spaces dropped
217def lstrip(ss: String) -> String:
218 String.from_list(lstrip.go(String.to_list(ss)))
219
220# not ""?
221def nonempty(ss: String) -> Bool:
222 Bool.not(String.is_empty(ss))
223
224# the words of a line
225def words(ss: String) -> List<&2, String>:
226 List.filter(~String, ~nonempty, String.split(ss, ' '))
227
228# a comment line without its `# `
229def undoc(+ss: String) -> String:
230 Bool.pick(String, String.starts_with(ss, "# "), String.drop(ss, 2n),
231 Bool.pick(String, String.starts_with(ss, "#"), String.drop(ss, 1n), ss))
232
233def undoc.all(doc: List<&2, String>, acc: List<&2, String>) -> List<&2, String>:
234 match doc:
235 case Nil{}:
236 acc
237 case Con{h, t}:
238 undoc.all(t, undoc(h) <> acc)
239
240# the comment block (reversed) as text
241def doc_text(doc: List<&2, String>) -> String:
242 String.join(undoc.all(doc, []), "\n")
243
244# the pending header lines (reversed) as one text
245def sig_text(sig: List<&2, String>) -> String:
246 String.join(List.reverse(&2, String, sig), "\n")
247
248# an item, when there is one, onto the items
249def add(mm: Maybe<&2, Item>, out: List<&2, Item>) -> List<&2, Item>:
250 match mm:
251 case None{}:
252 out
253 case Some{item}:
254 item <> out
255
256# `import <path> as <Alias>` is named by its alias; `import Base` by its path
257def import_item.of(ws: List<&2, String>, ss: String, nn: U32, doc: String) -> Maybe<&2, Item>:
258 match ws:
259 case Con{i, Con{path, Con{as, Con{alias, r}}}}:
260 Some{Item{IImport{}, alias, nn, ss, doc, path}}
261 case Con{i, Con{+path, Nil{}}}:
262 Some{Item{IImport{}, path, nn, ss, doc, path}}
263 case other:
264 None{}
265
266# an `import` line as an item: `import Base`, or `import <path> as <Alias>`
267def import_item(+ss: String, nn: U32, doc: String) -> Maybe<&2, Item>:
268 import_item.of(words(ss), ss, nn, doc)
269
270# the name of a `def` or `@unsafe def` line
271def def_name(+ss: String) -> String:
272 name(Bool.pick(String, String.starts_with(ss, "@"), String.drop(ss, 12n), String.drop(ss, 4n)))
273
274def starts_upper.of(cs: List<&2, Char>) -> Bool:
275 match cs:
276 case Con{c, t}:
277 Char.is_upper(c)
278 case Nil{}:
279 False{}
280
281# does the text start with a capital?
282def starts_upper(ss: String) -> Bool:
283 starts_upper.of(String.to_list(ss))
284
285# an indented line of a type block: `Ctor{..}`
286def ctor.of(+tt: String, nn: U32) -> Maybe<&2, Item>:
287 Lazy.stop(Maybe<&2, Item>, Bool.not(Bool.and(starts_upper(tt), String.contains(tt, "{"))), None{},
288 _u => Some{Item{ICtor{}, name(tt), nn, tt, "", ""}})
289
290# an indented line of a type block as a constructor item, when it is one
291def ctor(text: String, nn: U32) -> Maybe<&2, Item>:
292 ctor.of(lstrip(text), nn)
293
294# a def?
295def is_def(kk: ItemKind) -> Bool:
296 match kk:
297 case IDef{}:
298 True{}
299 case other:
300 False{}
301
302# what was pending becomes an item
303def flush(mode: Mode, out: List<&2, Item>) -> List<&2, Item>:
304 match mode:
305 case Pending{k, nm, l, d, sig}:
306 Item{k, nm, l, sig_text(sig), d, ""} <> out
307 case other:
308 out
309
310# a `:` token?
311def ends_colon.is(kk: Lex.TokKind) -> Bool:
312 match kk:
313 case Lex.TColon{}:
314 True{}
315 case other:
316 False{}
317
318# the tokens walked so far ended in `:`; a space or a comment keeps that
319def ends_colon.go(toks: List<&2, Lex.Tok>, prev: Bool) -> Bool:
320 match toks:
321 case Nil{}:
322 prev
323 case Con{Lex.Tok{+kk, tx, ln, cl}, t}:
324 ends_colon.go(t, Bool.pick(Bool, Lex.significant(kk), ends_colon.is(kk), prev))
325
326# the line's last token that is not a space or a comment is `:`, so a header
327# ends at its `:` though a comment follows it (`def f() -> U32: # note`)
328def ends_colon(ss: String) -> Bool:
329 ends_colon.go(Lex.tokens(ss), False{})
330
331# a def continuation that does or does not end in `:`
332def pend_more.closed(
333 colon: Bool,
334 nm: String,
335 ll: U32,
336 dd: String,
337 sig: List<&2, String>,
338 text: String,
339 nn: U32,
340 out: List<&2, Item>
341) -> St:
342 match colon:
343 case True{}:
344 St{Top{}, (nn + 1 : U32), [], Item{IDef{}, nm, ll, sig_text(text <> sig), dd, ""} <> out}
345 case False{}:
346 St{Pending{IDef{}, nm, ll, dd, text <> sig}, (nn + 1 : U32), [], out}
347
348# one more line of a def header
349def pend_more.def(
350 nm: String,
351 ll: U32,
352 dd: String,
353 sig: List<&2, String>,
354 +text: String,
355 nn: U32,
356 out: List<&2, Item>
357) -> St:
358 pend_more.closed(ends_colon(text), nm, ll, dd, sig, text, nn, out)
359
360# one more line of a pending header; a def's ends at the line that ends in `:`
361def pend_more(
362 kk: ItemKind,
363 nm: String,
364 ll: U32,
365 dd: String,
366 sig: List<&2, String>,
367 text: String,
368 nn: U32,
369 out: List<&2, Item>
370) -> St:
371 match kk:
372 case IDef{}:
373 pend_more.def(nm, ll, dd, sig, text, nn, out)
374 case other:
375 St{Pending{kk, nm, ll, dd, text <> sig}, (nn + 1 : U32), [], out}
376
377# an indented line: more of a pending header, a constructor of an open
378# type, or nothing
379def indent(mode: Mode, text: String, +nn: U32, out: List<&2, Item>) -> St:
380 match mode:
381 case Pending{k, nm, l, d, sig}:
382 pend_more(k, nm, l, d, sig, text, nn, out)
383 case InType{}:
384 St{InType{}, (nn + 1 : U32), [], add(ctor(text, nn), out)}
385 case Top{}:
386 St{Top{}, (nn + 1 : U32), [], out}
387
388# a column-0 line that starts nothing: `) -> T:` closing a def's header that
389# broke across lines; anywhere else it is junk, and ends what was pending
390def stray(+mode: Mode, text: String, +nn: U32, +out: List<&2, Item>) -> St:
391 match mode:
392 case Pending{IDef{}, nm, l, d, sig}:
393 pend_more(IDef{}, nm, l, d, sig, text, nn, out)
394 case m:
395 St{Top{}, (nn + 1 : U32), [], flush(m, out)}
396
397# a finished def, or a header that is still open
398def start_def.on(colon: Bool, +ss: String, +nn: U32, doc: String, out: List<&2, Item>) -> St:
399 match colon:
400 case True{}:
401 St{Top{}, (nn + 1 : U32), [], Item{IDef{}, def_name(ss), nn, ss, doc, ""} <> out}
402 case False{}:
403 St{Pending{IDef{}, def_name(ss), nn, doc, [ss]}, (nn + 1 : U32), [], out}
404
405# a `def` line: an item at once when it ends in `:`, else a pending header
406def start_def(+ss: String, +nn: U32, doc: String, out: List<&2, Item>) -> St:
407 start_def.on(ends_colon(ss), ss, nn, doc, out)
408
409# a comment continues the block above it only at the top level
410def doc_above(mode: Mode, doc: List<&2, String>) -> List<&2, String>:
411 match mode:
412 case Top{}:
413 doc
414 case other:
415 []
416
417# a law header, pending the block under it
418def step.law(+ss: String, +nn: U32, doc: String, mode: Mode, out: List<&2, Item>) -> St:
419 St{Pending{ILaw{}, name(String.drop(ss, 4n)), nn, doc, [ss]}, (nn + 1 : U32), [], flush(mode, out)}
420
421# a type header, and the block that follows it
422def step.ty(+ss: String, +nn: U32, doc: String, mode: Mode, out: List<&2, Item>) -> St:
423 St{InType{}, (nn + 1 : U32), [],
424 Item{IType{}, name(String.drop(ss, 5n)), nn, ss, doc, ""} <> flush(mode, out)}
425
426def step.go(cls: Cls, +mode: Mode, ss: String, +nn: U32, doc: List<&2, String>, out: List<&2, Item>) -> St:
427 match cls:
428 case LBlank{}:
429 St{Top{}, (nn + 1 : U32), [], flush(mode, out)}
430 case LComment{t}:
431 St{Top{}, (nn + 1 : U32), t <> doc_above(mode, doc), flush(mode, out)}
432 case LIndent{t}:
433 indent(mode, t, nn, out)
434 case LImport{}:
435 St{Top{}, (nn + 1 : U32), [], add(import_item(ss, nn, doc_text(doc)), flush(mode, out))}
436 case LDef{}:
437 start_def(ss, nn, doc_text(doc), flush(mode, out))
438 case LLaw{}:
439 step.law(ss, nn, doc_text(doc), mode, out)
440 case LType{}:
441 step.ty(ss, nn, doc_text(doc), mode, out)
442 case LOther{}:
443 stray(mode, ss, nn, out)
444
445# the class and the line, applied to the outline
446def step.use(pp: Cls & String, mode: Mode, +nn: U32, doc: List<&2, String>, out: List<&2, Item>) -> St:
447 (cls, s) = pp
448 step.go(cls, mode, s, nn, doc, out)
449
450# a line inside a string: more of a pending header, else nothing
451def step.text.go(mode: Mode, ss: String, +nn: U32, doc: List<&2, String>, out: List<&2, Item>) -> St:
452 match mode:
453 case Pending{k, nm, l, d, sig}:
454 St{Pending{k, nm, l, d, ss <> sig}, (nn + 1 : U32), doc, out}
455 case m:
456 St{m, (nn + 1 : U32), doc, out}
457
458# a line inside a string, applied to the outline
459def step.text(ss: String, st: St) -> St:
460 St{mode, n, doc, out} = st
461 step.text.go(mode, ss, n, doc, out)
462
463# a line of code, by its class
464def step.code(ss: String, st: St) -> St:
465 St{mode, n, doc, out} = st
466 step.use(classify.hold(ss), mode, n, doc, out)
467
468# one line, by its class, or as text when within says it starts inside a string
469def step(within: Bool, ss: String, st: St) -> St:
470 match within:
471 case True{}:
472 step.text(ss, st)
473 case False{}:
474 step.code(ss, st)
475
476# one flag per line break in a text, onto acc (reversed), each str: whether
477# the text is a string's
478def breaks(tt: String, +str: Bool, +acc: List<&2, Bool>) -> List<&2, Bool>:
479 match tt:
480 case SNil{}:
481 acc
482 case SCon{h, t}:
483 breaks(t, str, Bool.pick(List<&2, Bool>, U32.is_eq(Char.to_u32(h), 10), str <> acc, acc)) # '\n'
484
485def inside.go(toks: List<&2, Lex.Tok>, acc: List<&2, Bool>) -> List<&2, Bool>:
486 match toks:
487 case Nil{}:
488 acc
489 case Con{Lex.Tok{k, t, l, c}, rest}:
490 inside.go(rest, breaks(t, Lex.is_str(k), acc))
491
492# one flag per line, in order: does it start inside a string? Line 0 does
493# not; the line after a line break does when the break sits in a string token
494def inside(toks: List<&2, Lex.Tok>) -> List<&2, Bool>:
495 False{} <> List.reverse(&2, Bool, inside.go(toks, []))
496
497# does the line start inside a string? ins are the flags of it and the lines
498# after it; none left is no
499def inside.hit(ins: List<&2, Bool>) -> Bool:
500 match ins:
501 case Nil{}:
502 False{}
503 case Con{h, t}:
504 h
505
506# the flags of the lines after this one
507def inside.rest(ins: List<&2, Bool>) -> List<&2, Bool>:
508 match ins:
509 case Nil{}:
510 Nil{}
511 case Con{h, t}:
512 t
513
514# every line, in order; ins flags the lines ahead that start inside a string
515def run(lines: List<&2, String>, st: St, +ins: List<&2, Bool>) -> St:
516 match lines:
517 case Nil{}:
518 st
519 case Con{l, t}:
520 run(t, step(inside.hit(ins), l, st), inside.rest(ins))
521
522# the items once the lines are over, a pending header flushed
523def finish(st: St) -> List<&2, Item>:
524 St{mode, n, doc, out} = st
525 List.reverse(&2, Item, flush(mode, out))
526
527# the top-level items of a source whose tokens are already lexed, in order
528def of_tokens(text: String, toks: List<&2, Lex.Tok>) -> List<&2, Item>:
529 finish(run(String.lines(text), St{Top{}, 0, [], []}, inside(toks)))
530
531# the top-level items of a source, in order
532def items(+text: String) -> List<&2, Item>:
533 of_tokens(text, Lex.tokens(text))
534
535# lookups
536# -------
537
538# an import?
539def is_import(kk: ItemKind) -> Bool:
540 match kk:
541 case IImport{}:
542 True{}
543 case other:
544 False{}
545
546# the item of that name; imports are not names of the module
547def find(items: List<&2, Item>, +name: String) -> Maybe<&2, Item>:
548 match items:
549 case Nil{}:
550 None{}
551 case Con{Item{+k, +nm, l, s, d, p}, t}:
552 Lazy.stop(Maybe<&2, Item>, Bool.and(Bool.not(is_import(k)), String.eq(nm, name)),
553 Some{Item{k, nm, l, s, d, p}}, _u => find(t, name))
554
555# the path behind an import alias
556def import_path(items: List<&2, Item>, +alias: String) -> Maybe<&2, String>:
557 match items:
558 case Nil{}:
559 None{}
560 case Con{Item{k, nm, l, s, d, p}, t}:
561 Lazy.stop(Maybe<&2, String>, Bool.and(is_import(k), String.eq(nm, alias)),
562 Some{p}, _u => import_path(t, alias))
563
564# is the name among these?
565def has(names: List<&2, String>, +name: String) -> Bool:
566 match names:
567 case Nil{}:
568 False{}
569 case Con{h, t}:
570 Lazy.or_else(String.eq(h, name), _u => has(t, name))
571
572def starting.go(items: List<&2, Item>, +prefix: String, +qual: String, +seen: List<&2, String>) -> List<&2, Item>:
573 match items:
574 case Nil{}:
575 Nil{}
576 case Con{Item{+k, +nm, l, s, d, p}, t}:
577 +take = Bool.and(Bool.and(Bool.not(is_import(k)), String.starts_with(nm, prefix)), Bool.not(has(seen, nm)))
578 # one recursive call: Bool.pick evaluates both of its branches
579 +rest = starting.go(t, prefix, qual, Bool.pick(List<&2, String>, take, nm <> seen, seen))
580 Bool.pick(List<&2, Item>, take, Item{k, qual ++ nm, l, s, d, p} <> rest, rest)
581
582# the named items whose name starts with prefix, each renamed qual ++ name (the
583# text to insert). A law and the def that fills it share a name: the first
584# stands for both.
585def starting(items: List<&2, Item>, prefix: String, qual: String) -> List<&2, Item>:
586 starting.go(items, prefix, qual, [])
587
588# the import aliases that start with prefix (`import Base` has none)
589def aliases(items: List<&2, Item>, +prefix: String) -> List<&2, Item>:
590 match items:
591 case Nil{}:
592 Nil{}
593 case Con{Item{+k, +nm, l, s, d, +p}, t}:
594 +rest = aliases(t, prefix)
595 Bool.pick(List<&2, Item>,
596 Bool.and(Bool.and(is_import(k), String.starts_with(nm, prefix)), Bool.not(String.eq(nm, p))),
597 Item{k, nm, l, s, d, p} <> rest, rest)