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)