src/syntax/outline.bend

bolt@1.21.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.21.0.0/src/lazy/lazy.bend as Lazy
11import bolt@1.21.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)