~/bend-docscommunity

syntax/outline.bend source

syntax/outline.bend on the hub · documented module

# 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.import Baseimport ../lazy/lazy.bend as Lazy# 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>}# only whitespace?def is_blank(s: String) -> Bool:  List.all(~&2, ~Char, ~Char.is_space, String.to_list(s))# a line's classdef classify(+s: String) -> Cls:  Bool.pick(Cls, is_blank(s), LBlank{},    Bool.pick(Cls, String.starts_with(s, "#"), LComment{s},      Bool.pick(Cls, String.starts_with(s, " "), LIndent{s},        Bool.pick(Cls, String.starts_with(s, "import "), LImport{},          Bool.pick(Cls, Bool.or(String.starts_with(s, "def "), String.starts_with(s, "@unsafe def ")), LDef{},            Bool.pick(Cls, String.starts_with(s, "law "), LLaw{},              Bool.pick(Cls, String.starts_with(s, "type "), LType{}, LOther{})))))))# a name runs to the first of ( < : { or a spacedef name.go(cs: List<&2, Char>, acc: List<&2, Char>) -> List<&2, Char>:  match cs:    case Con{'(', t}:      acc    case Con{'<', t}:      acc    case Con{':', t}:      acc    case Con{'{', t}:      acc    case Con{' ', t}:      acc    case Con{c, t}:      name.go(t, c <> acc)    case Nil{}:      acc# the name that starts a header: up to `(`, `<`, `:`, `{` or a spacedef name(s: String) -> String:  String.from_list(List.reverse(&2, Char, name.go(String.to_list(s), [])))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(s: String) -> String:  String.from_list(lstrip.go(String.to_list(s)))# not ""?def nonempty(s: String) -> Bool:  Bool.not(String.is_empty(s))# the words of a linedef words(s: String) -> List<&2, String>:  List.filter(~String, ~nonempty, String.split(s, ' '))# a comment line without its `# `def undoc(+s: String) -> String:  Bool.pick(String, String.starts_with(s, "# "), String.drop(s, 2n),    Bool.pick(String, String.starts_with(s, "#"), String.drop(s, 1n), s))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(m: Maybe<&2, Item>, out: List<&2, Item>) -> List<&2, Item>:  match m:    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>, s: String, n: U32, doc: String) -> Maybe<&2, Item>:  match ws:    case Con{i, Con{path, Con{as, Con{alias, r}}}}:      Some{Item{IImport{}, alias, n, s, doc, path}}    case Con{i, Con{+path, Nil{}}}:      Some{Item{IImport{}, path, n, s, doc, path}}    case other:      None{}# an `import` line as an item: `import Base`, or `import <path> as <Alias>`def import_item(+s: String, n: U32, doc: String) -> Maybe<&2, Item>:  import_item.of(words(s), s, n, doc)# the name of a `def` or `@unsafe def` linedef def_name(+s: String) -> String:  name(Bool.pick(String, String.starts_with(s, "@"), String.drop(s, 12n), String.drop(s, 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(s: String) -> Bool:  starts_upper.of(String.to_list(s))# an indented line of a type block: `Ctor{..}`def ctor.of(+t: String, n: U32) -> Maybe<&2, Item>:  Lazy.stop(Maybe<&2, Item>, Bool.not(Bool.and(starts_upper(t), String.contains(t, "{"))), None{},    _u => Some{Item{ICtor{}, name(t), n, t, "", ""}})# an indented line of a type block as a constructor item, when it is onedef ctor(text: String, n: U32) -> Maybe<&2, Item>:  ctor.of(lstrip(text), n)# a def?def is_def(k: ItemKind) -> Bool:  match k:    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# one more line of a pending header; a def's ends at the line that ends in `:`def pend_more(+k: ItemKind, +nm: String, +l: U32, +d: String, +sig: List<&2, String>, +text: String, +n: U32,  +out: List<&2, Item>) -> St:  Bool.pick(St, Bool.and(is_def(k), String.ends_with(text, ":")),    St{Top{}, (n + 1 : U32), [], Item{k, nm, l, sig_text(text <> sig), d, ""} <> out},    St{Pending{k, nm, l, d, text <> sig}, (n + 1 : U32), [], out})# an indented line: more of a pending header, a constructor of an open# type, or nothingdef indent(mode: Mode, text: String, +n: U32, out: List<&2, Item>) -> St:  match mode:    case Pending{k, nm, l, d, sig}:      pend_more(k, nm, l, d, sig, text, n, out)    case InType{}:      St{InType{}, (n + 1 : U32), [], add(ctor(text, n), out)}    case Top{}:      St{Top{}, (n + 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, +n: U32, +out: List<&2, Item>) -> St:  match mode:    case Pending{IDef{}, nm, l, d, sig}:      pend_more(IDef{}, nm, l, d, sig, text, n, out)    case m:      St{Top{}, (n + 1 : U32), [], flush(m, out)}# a `def` line: an item at once when it ends in `:`, else a pending headerdef start_def(+s: String, +n: U32, +doc: String, +out: List<&2, Item>) -> St:  +nm = def_name(s)  Bool.pick(St, String.ends_with(s, ":"),    St{Top{}, (n + 1 : U32), [], Item{IDef{}, nm, n, s, doc, ""} <> out},    St{Pending{IDef{}, nm, n, doc, [s]}, (n + 1 : U32), [], 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:      []def step.go(cls: Cls, +mode: Mode, +s: String, +n: U32, doc: List<&2, String>, out: List<&2, Item>) -> St:  match cls:    case LBlank{}:      St{Top{}, (n + 1 : U32), [], flush(mode, out)}    case LComment{t}:      St{Top{}, (n + 1 : U32), t <> doc_above(mode, doc), flush(mode, out)}    case LIndent{t}:      indent(mode, t, n, out)    case LImport{}:      St{Top{}, (n + 1 : U32), [], add(import_item(s, n, doc_text(doc)), flush(mode, out))}    case LDef{}:      start_def(s, n, doc_text(doc), flush(mode, out))    case LLaw{}:      St{Pending{ILaw{}, name(String.drop(s, 4n)), n, doc_text(doc), [s]}, (n + 1 : U32), [], flush(mode, out)}    case LType{}:      St{InType{}, (n + 1 : U32), [],        Item{IType{}, name(String.drop(s, 5n)), n, s, doc_text(doc), ""} <> flush(mode, out)}    case LOther{}:      stray(mode, s, n, out)# one line, by its classdef step(+s: String, st: St) -> St:  St{mode, n, doc, out} = st  step.go(classify(s), mode, s, n, doc, out)# every line, in orderdef run(lines: List<&2, String>, st: St) -> St:  match lines:    case Nil{}:      st    case Con{l, t}:      run(t, step(l, st))# 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, in orderdef items(text: String) -> List<&2, Item>:  finish(run(String.lines(text), St{Top{}, 0, [], []}))# lookups# -------# an import?def is_import(k: ItemKind) -> Bool:  match k:    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)