~/bend-docscommunity

src/syntax/outline.bend source

src/syntax/outline.bend on the hub · documented module

# src/syntax/outline: a source's top-level items, read line by line. A top-level# item starts at column 0, so a line the outline does not understand costs# nothing: it is skipped, and the items around it still stand. That tolerance# is the point: an editor asks about files that are half-written.## A line that starts inside a string literal (the lexer's, which runs across# newlines as bend's does) is text, not a line of code: it is never an item,# a comment or a blank, and it joins a pending header as one more line.import Baseimport ../lazy/lazy.bend as Lazyimport ./lex.bend as Lex# what an item is; ILocal is not read from a source, it is how the server# offers a binder as a candidatetype ItemKind is Data:  IImport{}  IDef{}  ILaw{}  IType{}  ICtor{}  ILocal{}# line is 0-based; sig is the header text (a def's through its `:`, a law's# whole block, a constructor's line); doc is the comment block right above;# path is an import's targettype Item is Data:  Item{kind: ItemKind, name: String, line: U32, sig: String, doc: String, path: String}# what a line is, by its first charstype Cls is Data:  LBlank{}  LComment{text: String}  LIndent{text: String}  LImport{}  LDef{}  LLaw{}  LType{}  LOther{}# Pending: a def's header that has not reached its `:`, or a law's blocktype Mode is Data:  Top{}  InType{}  Pending{kind: ItemKind, name: String, line: U32, doc: String, sig: List<&2, String>}# doc and out are reversedtype St is Data:  St{mode: Mode, n: U32, doc: List<&2, String>, out: List<&2, Item>}# the class when the prefix matches, else otherdef classify.kw(hit: Bool, yes: Cls) -> Cls:  match hit:    case True{}:      yes    case False{}:      LOther{}# leading spaces (reversed) in front of the rest of the linedef classify.rebuild(acc: List<&2, Char>, rest: String) -> String:  match acc:    case Nil{}:      rest    case Con{h, t}:      classify.rebuild(t, SCon{h, rest})# an indented line when the first char is a space, else otherdef classify.white.hit(indent: Bool, acc: List<&2, Char>, hh: Char, rest: String) -> Cls:  match indent:    case True{}:      LIndent{classify.rebuild(acc, SCon{hh, rest})}    case False{}:      LOther{}# whitespace after a leading space-class char. `space` says `h` is whitespace;# `acc` is the whitespace before `h`, reversed; `indent` when that line opened# with a spacedef classify.white(tt: String, space: Bool, hh: Char, indent: Bool, acc: List<&2, Char>) -> Cls:  match tt space:    case SNil{} True{}:      LBlank{}    case SNil{} False{}:      classify.white.hit(indent, acc, hh, SNil{})    case SCon{+h2, r} True{}:      classify.white(r, Char.is_space(h2), h2, indent, hh <> acc)    case SCon{h2, r} False{}:      classify.white.hit(indent, acc, hh, SCon{h2, r})# a line whose first char is whitespacedef classify.white.enter(tt: String, +xx: U32) -> Cls:  classify.white(tt, True{}, Char.from_u32(xx), U32.is_eq(xx, 32), []) # ' '# `type `, once the `t` is knowndef classify.ktype(hit: Bool, tt: String) -> Cls:  match hit:    case True{}:      classify.kw(String.starts_with(tt, "ype "), LType{})    case False{}:      LOther{}# `law `, else `type `def classify.klaw(hit: Bool, +xx: U32, tt: String) -> Cls:  match hit:    case True{}:      classify.kw(String.starts_with(tt, "aw "), LLaw{})    case False{}:      classify.ktype(U32.is_eq(xx, 116), tt) # 't'# `@unsafe def `, else `law `def classify.kat(hit: Bool, +xx: U32, tt: String) -> Cls:  match hit:    case True{}:      classify.kw(String.starts_with(tt, "unsafe def "), LDef{})    case False{}:      classify.klaw(U32.is_eq(xx, 108), xx, tt) # 'l'# `def `, else `@unsafe def `def classify.kdef(hit: Bool, +xx: U32, tt: String) -> Cls:  match hit:    case True{}:      classify.kw(String.starts_with(tt, "ef "), LDef{})    case False{}:      classify.kat(U32.is_eq(xx, 64), xx, tt) # '@'# `import `, else `def `def classify.kimp(hit: Bool, +xx: U32, tt: String) -> Cls:  match hit:    case True{}:      classify.kw(String.starts_with(tt, "mport "), LImport{})    case False{}:      classify.kdef(U32.is_eq(xx, 100), xx, tt) # 'd'# a column-0 keyword, from the first character's codedef classify.keys(+xx: U32, tt: String) -> Cls:  classify.kimp(U32.is_eq(xx, 105), xx, tt) # 'i'# a column-0 keyword keeps the line; the class comes from its first characterdef classify.hold.keys(+xx: U32, cc: Char, +tt: String) -> Cls & String:  (classify.keys(xx, tt), SCon{cc, tt})# whitespace that is not a leading space keeps the line: the class may notdef classify.hold.gap.keep(+xx: U32, cc: Char, +tt: String) -> Cls & String:  (classify.white.enter(tt, xx), SCon{cc, tt})# a leading space is blank or indented, and that class already carries the linedef classify.hold.gap(indent: Bool, xx: U32, cc: Char, tt: String) -> Cls & String:  match indent:    case True{}:      (classify.white.enter(tt, xx), SNil{})    case False{}:      classify.hold.gap.keep(xx, cc, tt)# whitespace, else a keyworddef classify.hold.space(space: Bool, +xx: U32, cc: Char, tt: String) -> Cls & String:  match space:    case True{}:      classify.hold.gap(U32.is_eq(xx, 32), xx, cc, tt) # ' '    case False{}:      classify.hold.keys(xx, cc, tt)# a comment keeps its text; anything else is whitespace or a keyworddef classify.hold.hash(is_hash: Bool, space: Bool, xx: U32, cc: Char, tt: String) -> Cls & String:  match is_hash:    case True{}:      (LComment{SCon{cc, tt}}, SNil{})    case False{}:      classify.hold.space(space, xx, cc, tt)# the first character, then the rest of the linedef classify.hold.open(+xx: U32, +cc: Char, tt: String) -> Cls & String:  classify.hold.hash(U32.is_eq(xx, 35), Char.is_space(cc), xx, cc, tt) # '#'# the class, and the line when the class does not already carry itdef classify.hold(ss: String) -> Cls & String:  match ss:    case SNil{}:      (LBlank{}, SNil{})    case SCon{+c, t}:      classify.hold.open(Char.to_u32(c), c, t)# `(`, `<`, `:`, `{` or a space ends a namedef name.stop(+hh: Char) -> Bool:  Bool.or(Char.is_eq(hh, '('),    Bool.or(Char.is_eq(hh, '<'),      Bool.or(Char.is_eq(hh, ':'),        Bool.or(Char.is_eq(hh, '{'), Char.is_eq(hh, ' ')))))# a delimiter is not part of the namedef name.push(stop: Bool, hh: Char, acc: String) -> String:  match stop:    case True{}:      acc    case False{}:      SCon{hh, acc}# the name, reversed into acc, up to a delimiterdef name.go(ss: String, acc: String, stop: Bool) -> String:  match ss stop:    case SNil{} _:      String.reverse(acc)    case SCon{h, t} True{}:      String.reverse(acc)    case SCon{+h, t} False{}:      name.go(t, name.push(name.stop(h), h, acc), name.stop(h))# the name that starts a header: up to `(`, `<`, `:`, `{` or a spacedef name(ss: String) -> String:  name.go(ss, SNil{}, False{})def lstrip.go(cs: List<&2, Char>) -> List<&2, Char>:  match cs:    case Con{' ', t}:      lstrip.go(t)    case other:      other# leading spaces droppeddef lstrip(ss: String) -> String:  String.from_list(lstrip.go(String.to_list(ss)))# not ""?def nonempty(ss: String) -> Bool:  Bool.not(String.is_empty(ss))# the words of a linedef words(ss: String) -> List<&2, String>:  List.filter(~String, ~nonempty, String.split(ss, ' '))# a comment line without its `# `def undoc(+ss: String) -> String:  Bool.pick(String, String.starts_with(ss, "# "), String.drop(ss, 2n),    Bool.pick(String, String.starts_with(ss, "#"), String.drop(ss, 1n), ss))def undoc.all(doc: List<&2, String>, acc: List<&2, String>) -> List<&2, String>:  match doc:    case Nil{}:      acc    case Con{h, t}:      undoc.all(t, undoc(h) <> acc)# the comment block (reversed) as textdef doc_text(doc: List<&2, String>) -> String:  String.join(undoc.all(doc, []), "\n")# the pending header lines (reversed) as one textdef sig_text(sig: List<&2, String>) -> String:  String.join(List.reverse(&2, String, sig), "\n")# an item, when there is one, onto the itemsdef add(mm: Maybe<&2, Item>, out: List<&2, Item>) -> List<&2, Item>:  match mm:    case None{}:      out    case Some{item}:      item <> out# `import <path> as <Alias>` is named by its alias; `import Base` by its pathdef import_item.of(ws: List<&2, String>, ss: String, nn: U32, doc: String) -> Maybe<&2, Item>:  match ws:    case Con{i, Con{path, Con{as, Con{alias, r}}}}:      Some{Item{IImport{}, alias, nn, ss, doc, path}}    case Con{i, Con{+path, Nil{}}}:      Some{Item{IImport{}, path, nn, ss, doc, path}}    case other:      None{}# an `import` line as an item: `import Base`, or `import <path> as <Alias>`def import_item(+ss: String, nn: U32, doc: String) -> Maybe<&2, Item>:  import_item.of(words(ss), ss, nn, doc)# the name of a `def` or `@unsafe def` linedef def_name(+ss: String) -> String:  name(Bool.pick(String, String.starts_with(ss, "@"), String.drop(ss, 12n), String.drop(ss, 4n)))def starts_upper.of(cs: List<&2, Char>) -> Bool:  match cs:    case Con{c, t}:      Char.is_upper(c)    case Nil{}:      False{}# does the text start with a capital?def starts_upper(ss: String) -> Bool:  starts_upper.of(String.to_list(ss))# an indented line of a type block: `Ctor{..}`def ctor.of(+tt: String, nn: U32) -> Maybe<&2, Item>:  Lazy.stop(Maybe<&2, Item>, Bool.not(Bool.and(starts_upper(tt), String.contains(tt, "{"))), None{},    _u => Some{Item{ICtor{}, name(tt), nn, tt, "", ""}})# an indented line of a type block as a constructor item, when it is onedef ctor(text: String, nn: U32) -> Maybe<&2, Item>:  ctor.of(lstrip(text), nn)# a def?def is_def(kk: ItemKind) -> Bool:  match kk:    case IDef{}:      True{}    case other:      False{}# what was pending becomes an itemdef flush(mode: Mode, out: List<&2, Item>) -> List<&2, Item>:  match mode:    case Pending{k, nm, l, d, sig}:      Item{k, nm, l, sig_text(sig), d, ""} <> out    case other:      out# a `:` token?def ends_colon.is(kk: Lex.TokKind) -> Bool:  match kk:    case Lex.TColon{}:      True{}    case other:      False{}# the tokens walked so far ended in `:`; a space or a comment keeps thatdef ends_colon.go(toks: List<&2, Lex.Tok>, prev: Bool) -> Bool:  match toks:    case Nil{}:      prev    case Con{Lex.Tok{+kk, tx, ln, cl}, t}:      ends_colon.go(t, Bool.pick(Bool, Lex.significant(kk), ends_colon.is(kk), prev))# the line's last token that is not a space or a comment is `:`, so a header# ends at its `:` though a comment follows it (`def f() -> U32: # note`)def ends_colon(ss: String) -> Bool:  ends_colon.go(Lex.tokens(ss), False{})# a def continuation that does or does not end in `:`def pend_more.closed(  colon: Bool,  nm: String,  ll: U32,  dd: String,  sig: List<&2, String>,  text: String,  nn: U32,  out: List<&2, Item>) -> St:  match colon:    case True{}:      St{Top{}, (nn + 1 : U32), [], Item{IDef{}, nm, ll, sig_text(text <> sig), dd, ""} <> out}    case False{}:      St{Pending{IDef{}, nm, ll, dd, text <> sig}, (nn + 1 : U32), [], out}# one more line of a def headerdef pend_more.def(  nm: String,  ll: U32,  dd: String,  sig: List<&2, String>,  +text: String,  nn: U32,  out: List<&2, Item>) -> St:  pend_more.closed(ends_colon(text), nm, ll, dd, sig, text, nn, out)# one more line of a pending header; a def's ends at the line that ends in `:`def pend_more(  kk: ItemKind,  nm: String,  ll: U32,  dd: String,  sig: List<&2, String>,  text: String,  nn: U32,  out: List<&2, Item>) -> St:  match kk:    case IDef{}:      pend_more.def(nm, ll, dd, sig, text, nn, out)    case other:      St{Pending{kk, nm, ll, dd, text <> sig}, (nn + 1 : U32), [], out}# an indented line: more of a pending header, a constructor of an open# type, or nothingdef indent(mode: Mode, text: String, +nn: U32, out: List<&2, Item>) -> St:  match mode:    case Pending{k, nm, l, d, sig}:      pend_more(k, nm, l, d, sig, text, nn, out)    case InType{}:      St{InType{}, (nn + 1 : U32), [], add(ctor(text, nn), out)}    case Top{}:      St{Top{}, (nn + 1 : U32), [], out}# a column-0 line that starts nothing: `) -> T:` closing a def's header that# broke across lines; anywhere else it is junk, and ends what was pendingdef stray(+mode: Mode, text: String, +nn: U32, +out: List<&2, Item>) -> St:  match mode:    case Pending{IDef{}, nm, l, d, sig}:      pend_more(IDef{}, nm, l, d, sig, text, nn, out)    case m:      St{Top{}, (nn + 1 : U32), [], flush(m, out)}# a finished def, or a header that is still opendef start_def.on(colon: Bool, +ss: String, +nn: U32, doc: String, out: List<&2, Item>) -> St:  match colon:    case True{}:      St{Top{}, (nn + 1 : U32), [], Item{IDef{}, def_name(ss), nn, ss, doc, ""} <> out}    case False{}:      St{Pending{IDef{}, def_name(ss), nn, doc, [ss]}, (nn + 1 : U32), [], out}# a `def` line: an item at once when it ends in `:`, else a pending headerdef start_def(+ss: String, +nn: U32, doc: String, out: List<&2, Item>) -> St:  start_def.on(ends_colon(ss), ss, nn, doc, out)# a comment continues the block above it only at the top leveldef doc_above(mode: Mode, doc: List<&2, String>) -> List<&2, String>:  match mode:    case Top{}:      doc    case other:      []# a law header, pending the block under itdef step.law(+ss: String, +nn: U32, doc: String, mode: Mode, out: List<&2, Item>) -> St:  St{Pending{ILaw{}, name(String.drop(ss, 4n)), nn, doc, [ss]}, (nn + 1 : U32), [], flush(mode, out)}# a type header, and the block that follows itdef step.ty(+ss: String, +nn: U32, doc: String, mode: Mode, out: List<&2, Item>) -> St:  St{InType{}, (nn + 1 : U32), [],    Item{IType{}, name(String.drop(ss, 5n)), nn, ss, doc, ""} <> flush(mode, out)}def step.go(cls: Cls, +mode: Mode, ss: String, +nn: U32, doc: List<&2, String>, out: List<&2, Item>) -> St:  match cls:    case LBlank{}:      St{Top{}, (nn + 1 : U32), [], flush(mode, out)}    case LComment{t}:      St{Top{}, (nn + 1 : U32), t <> doc_above(mode, doc), flush(mode, out)}    case LIndent{t}:      indent(mode, t, nn, out)    case LImport{}:      St{Top{}, (nn + 1 : U32), [], add(import_item(ss, nn, doc_text(doc)), flush(mode, out))}    case LDef{}:      start_def(ss, nn, doc_text(doc), flush(mode, out))    case LLaw{}:      step.law(ss, nn, doc_text(doc), mode, out)    case LType{}:      step.ty(ss, nn, doc_text(doc), mode, out)    case LOther{}:      stray(mode, ss, nn, out)# the class and the line, applied to the outlinedef step.use(pp: Cls & String, mode: Mode, +nn: U32, doc: List<&2, String>, out: List<&2, Item>) -> St:  (cls, s) = pp  step.go(cls, mode, s, nn, doc, out)# a line inside a string: more of a pending header, else nothingdef step.text.go(mode: Mode, ss: String, +nn: U32, doc: List<&2, String>, out: List<&2, Item>) -> St:  match mode:    case Pending{k, nm, l, d, sig}:      St{Pending{k, nm, l, d, ss <> sig}, (nn + 1 : U32), doc, out}    case m:      St{m, (nn + 1 : U32), doc, out}# a line inside a string, applied to the outlinedef step.text(ss: String, st: St) -> St:  St{mode, n, doc, out} = st  step.text.go(mode, ss, n, doc, out)# a line of code, by its classdef step.code(ss: String, st: St) -> St:  St{mode, n, doc, out} = st  step.use(classify.hold(ss), mode, n, doc, out)# one line, by its class, or as text when within says it starts inside a stringdef step(within: Bool, ss: String, st: St) -> St:  match within:    case True{}:      step.text(ss, st)    case False{}:      step.code(ss, st)# one flag per line break in a text, onto acc (reversed), each str: whether# the text is a string'sdef breaks(tt: String, +str: Bool, +acc: List<&2, Bool>) -> List<&2, Bool>:  match tt:    case SNil{}:      acc    case SCon{h, t}:      breaks(t, str, Bool.pick(List<&2, Bool>, U32.is_eq(Char.to_u32(h), 10), str <> acc, acc)) # '\n'def inside.go(toks: List<&2, Lex.Tok>, acc: List<&2, Bool>) -> List<&2, Bool>:  match toks:    case Nil{}:      acc    case Con{Lex.Tok{k, t, l, c}, rest}:      inside.go(rest, breaks(t, Lex.is_str(k), acc))# one flag per line, in order: does it start inside a string? Line 0 does# not; the line after a line break does when the break sits in a string tokendef inside(toks: List<&2, Lex.Tok>) -> List<&2, Bool>:  False{} <> List.reverse(&2, Bool, inside.go(toks, []))# does the line start inside a string? ins are the flags of it and the lines# after it; none left is nodef inside.hit(ins: List<&2, Bool>) -> Bool:  match ins:    case Nil{}:      False{}    case Con{h, t}:      h# the flags of the lines after this onedef inside.rest(ins: List<&2, Bool>) -> List<&2, Bool>:  match ins:    case Nil{}:      Nil{}    case Con{h, t}:      t# every line, in order; ins flags the lines ahead that start inside a stringdef run(lines: List<&2, String>, st: St, +ins: List<&2, Bool>) -> St:  match lines:    case Nil{}:      st    case Con{l, t}:      run(t, step(inside.hit(ins), l, st), inside.rest(ins))# the items once the lines are over, a pending header flusheddef finish(st: St) -> List<&2, Item>:  St{mode, n, doc, out} = st  List.reverse(&2, Item, flush(mode, out))# the top-level items of a source whose tokens are already lexed, in orderdef of_tokens(text: String, toks: List<&2, Lex.Tok>) -> List<&2, Item>:  finish(run(String.lines(text), St{Top{}, 0, [], []}, inside(toks)))# the top-level items of a source, in orderdef items(+text: String) -> List<&2, Item>:  of_tokens(text, Lex.tokens(text))# lookups# -------# an import?def is_import(kk: ItemKind) -> Bool:  match kk:    case IImport{}:      True{}    case other:      False{}# the item of that name; imports are not names of the moduledef find(items: List<&2, Item>, +name: String) -> Maybe<&2, Item>:  match items:    case Nil{}:      None{}    case Con{Item{+k, +nm, l, s, d, p}, t}:      Lazy.stop(Maybe<&2, Item>, Bool.and(Bool.not(is_import(k)), String.eq(nm, name)),        Some{Item{k, nm, l, s, d, p}}, _u => find(t, name))# the path behind an import aliasdef import_path(items: List<&2, Item>, +alias: String) -> Maybe<&2, String>:  match items:    case Nil{}:      None{}    case Con{Item{k, nm, l, s, d, p}, t}:      Lazy.stop(Maybe<&2, String>, Bool.and(is_import(k), String.eq(nm, alias)),        Some{p}, _u => import_path(t, alias))# is the name among these?def has(names: List<&2, String>, +name: String) -> Bool:  match names:    case Nil{}:      False{}    case Con{h, t}:      Lazy.or_else(String.eq(h, name), _u => has(t, name))def starting.go(items: List<&2, Item>, +prefix: String, +qual: String, +seen: List<&2, String>) -> List<&2, Item>:  match items:    case Nil{}:      Nil{}    case Con{Item{+k, +nm, l, s, d, p}, t}:      +take = Bool.and(Bool.and(Bool.not(is_import(k)), String.starts_with(nm, prefix)), Bool.not(has(seen, nm)))      # one recursive call: Bool.pick evaluates both of its branches      +rest = starting.go(t, prefix, qual, Bool.pick(List<&2, String>, take, nm <> seen, seen))      Bool.pick(List<&2, Item>, take, Item{k, qual ++ nm, l, s, d, p} <> rest, rest)# the named items whose name starts with prefix, each renamed qual ++ name (the# text to insert). A law and the def that fills it share a name: the first# stands for both.def starting(items: List<&2, Item>, prefix: String, qual: String) -> List<&2, Item>:  starting.go(items, prefix, qual, [])# the import aliases that start with prefix (`import Base` has none)def aliases(items: List<&2, Item>, +prefix: String) -> List<&2, Item>:  match items:    case Nil{}:      Nil{}    case Con{Item{+k, +nm, l, s, d, +p}, t}:      +rest = aliases(t, prefix)      Bool.pick(List<&2, Item>,        Bool.and(Bool.and(is_import(k), String.starts_with(nm, prefix)), Bool.not(String.eq(nm, p))),        Item{k, nm, l, s, d, p} <> rest, rest)