src/rules/digest.bend

bolt@1.21.0.0 · 406 lines · 0 definitions

1# src/rules/digest: one small reading of each file for the project rules.
2# A project rule sees every file at once, and a `+` reuse of that list copies
3# it: handed the parsed `Src` (src/src.bend), each extra use duplicated every
4# file's text, tokens and tree, and the pair of project rules grew faster than
5# the file count (100 files cost 362 s that way). So each file is read down
6# once to what those rules ask of it -- its path, its top-level defs and types
7# by name and line, what its laws name, what it imports, and its `@unsafe`
8# and foreign defs -- and the phases run over these, which are strings and cheap to copy.
9import Base
10import bolt@1.21.0.0/src/src.bend as Src
11import bolt@1.21.0.0/src/syntax/lex.bend as Lex
12import bolt@1.21.0.0/src/syntax/tree.bend as Tree
13import bolt@1.21.0.0/src/syntax/bind.bend as Bind
14import bolt@1.21.0.0/src/syntax/outline.bend as Outline
15import bolt@1.21.0.0/src/lazy/lazy.bend as Lazy
16import bolt@1.21.0.0/src/rules/imports.bend as Imports
17import bolt@1.21.0.0/src/rules/laws/closed.bend as Closed
18import bolt@1.21.0.0/src/paths.bend as Paths
19import bolt@1.21.0.0/src/rules/correctness/foreign.bend as Foreign
20import bolt@1.21.0.0/src/rules/calls.bend as Calls
21
22# what a file holds
23# -----------------
24
25# a law names an item of a module: its path and its name
26type Mention is Data:
27  Mention{path: String, name: String}
28
29# a top-level def or type, by kind, name and the line it starts on, with a
30# type's constructors by name (none for a def), and what the uses on its
31# lines name (its calls, for a def): each as the binder resolved it, an item
32# of the file or one behind an import alias
33type Top is Data:
34  Top{kind: Outline.ItemKind, name: String, line: U32, ctors: List<&2, String>, calls: List<&2, Mention>}
35
36# a def a proof may not reach (bend 2.0.32): an `@unsafe def`, where its
37# `@` is, or a foreign def (its body only `import "./x.c"` / `import
38# "./x.js"` lines), where its header starts; foreign says which
39type Unsafe is Data:
40  Unsafe{name: String, line: U32, col: U32, foreign: Bool}
41
42# a law of a LAWS.bend: its name, the line it starts on, whether it binds
43# (a `for` or `exs` line), and the lines of its comment block, `# ` dropped
44type Law is Data:
45  Law{name: String, line: U32, binds: Bool, doc: List<&2, String>}
46
47# one file as the project rules see it: its path as given and normalized, its
48# directory, whether it states laws (LAWS.bend) or proves them too
49# (PROOF.bend), whether the coverage rule lets it off, its top-level items, what
50# its own laws name, the files it imports, its `@unsafe` and foreign defs
51type Digest is Data:
52  Digest{path: String, norm: String, dir: String, is_laws: Bool, is_law_file: Bool, exempt: Bool,
53    tops: List<&2, Top>, says: List<&2, Mention>, deps: List<&2, String>, unsafes: List<&2, Unsafe>,
54    laws: List<&2, Law>}
55
56# what the laws name
57# ------------------
58
59# an import alias and the path it names
60type Alias is Data:
61  Alias{name: String, path: String}
62
63# the aliases a file imports
64def aliases(items: List<&2, Outline.Item>, +from: String) -> List<&2, Alias>:
65  match items:
66    case Nil{}:
67      Nil{}
68    case Con{Outline.Item{Outline.IImport{}, name, line, sig, doc, path}, rest}:
69      Alias{name, Imports.resolve(from, path)} <> aliases(rest, from)
70    case Con{other, rest}:
71      aliases(rest, from)
72
73# the path an alias names, when it is one
74def alias_path(as: List<&2, Alias>, +name: String) -> Maybe<&2, String>:
75  match as:
76    case Nil{}:
77      None{}
78    case Con{Alias{+n, p}, rest}:
79      +more = alias_path(rest, name)
80      Bool.pick(Maybe<&2, String>, String.eq(n, name), Some{p}, more)
81
82# the lines a quantified law's statement spans: from its header's first token
83# to its body's last
84type Span is Data:
85  Span{from: U32, upto: U32}
86
87# the spans of every quantified law of a tree, in order. A closed law spans
88# nothing (it covers one input, not the def)
89def spans(root: Tree.Node) -> List<&2, Span>:
90  match root:
91    case Tree.NCons{Tree.Stmt{Tree.SLaw{}, kids, +body}, rest}:
92      +more = spans(rest)
93      Bool.pick(List<&2, Span>, Closed.binds(body),
94        Span{Tree.line(kids), Tree.line.of(List.head(&2, Lex.Tok, Tree.leaves.go(body, [])))} <> more, more)
95    case Tree.NCons{h, rest}:
96      spans(rest)
97    case other:
98      Nil{}
99
100# is the line in one of the spans?
101def within(ss: List<&2, Span>, +ln: U32) -> Bool:
102  match ss:
103    case Nil{}:
104      False{}
105    case Con{Span{+from, +upto}, rest}:
106      Lazy.or_else(Bool.and(U32.is_le(from, ln), U32.is_le(ln, upto)), _u => within(rest, ln))
107
108# an item behind an alias, when an import names the alias, onto the others
109def aliased(mm: Maybe<&2, String>, +name: String, more: List<&2, Mention>) -> List<&2, Mention>:
110  match mm:
111    case Some{p}:
112      Mention{p, name} <> more
113    case None{}:
114      more
115
116# what the binder resolved a use to, as a mention onto the others: an item of
117# the law's own file, or an item of the file an import alias names; nothing
118# for a binder, a free name, or an alias no import names
119def target(tt: Bind.Target, +as: List<&2, Alias>, +file: String, more: List<&2, Mention>) -> List<&2, Mention>:
120  match tt:
121    case Bind.TItem{n}:
122      Mention{file, n} <> more
123    case Bind.TQual{a, n}:
124      aliased(alias_path(as, a), n, more)
125    case other:
126      more
127
128# the mentions of every use the binder records in the statement of a
129# quantified law, in order. Its header's own name is a binder, not a use
130def mentions(us: List<&2, Bind.Use>, +ss: List<&2, Span>, +as: List<&2, Alias>, +file: String) -> List<&2, Mention>:
131  match us:
132    case Nil{}:
133      Nil{}
134    case Con{Bind.Use{n, +l, c, tt}, rest}:
135      +more = mentions(rest, ss, as, file)
136      +out = Bool.not(within(ss, l))
137      Lazy.stop(List<&2, Mention>, out, more, _u => target(tt, as, file, more))
138
139# the uses the binder recorded
140def uses.of(bb: Bind.Bound) -> List<&2, Bind.Use>:
141  match bb:
142    case Bind.Bound{bs, us, scs}:
143      us
144
145# what the file holds besides
146# ---------------------------
147
148# the names of the constructors the items open with: a type's, since the
149# outline lists them right after it
150def ctors(items: List<&2, Outline.Item>) -> List<&2, String>:
151  match items:
152    case Con{Outline.Item{Outline.ICtor{}, name, line, sig, doc, path}, rest}:
153      name <> ctors(rest)
154    case other:
155      Nil{}
156
157# the uses, in order, before the first one at or past the line
158def before(us: List<&2, Bind.Use>, +at: U32) -> List<&2, Bind.Use>:
159  match us:
160    case Nil{}:
161      Nil{}
162    case Con{Bind.Use{nm, +l, c, tt}, rest}:
163      Lazy.stop(List<&2, Bind.Use>, U32.is_le(at, l), [], _u => Bind.Use{nm, l, c, tt} <> before(rest, at))
164
165# the uses from the first one at or past the line on
166def after(us: List<&2, Bind.Use>, +at: U32) -> List<&2, Bind.Use>:
167  match us:
168    case Nil{}:
169      Nil{}
170    case Con{Bind.Use{nm, +l, c, tt}, +rest}:
171      Lazy.stop(List<&2, Bind.Use>, U32.is_le(at, l), Bind.Use{nm, l, c, tt} <> rest, _u => after(rest, at))
172
173# the uses before the line, when there is one; else all of them
174def upto(us: List<&2, Bind.Use>, mm: Maybe<&2, U32>) -> List<&2, Bind.Use>:
175  match mm:
176    case None{}:
177      us
178    case Some{at}:
179      before(us, at)
180
181# the uses from the line on, when there is one; else none
182def past(us: List<&2, Bind.Use>, mm: Maybe<&2, U32>) -> List<&2, Bind.Use>:
183  match mm:
184    case None{}:
185      Nil{}
186    case Some{at}:
187      after(us, at)
188
189# the line of the next item that ends an item's lines: any but a constructor,
190# which the outline lists inside its type
191def next_line(items: List<&2, Outline.Item>) -> Maybe<&2, U32>:
192  match items:
193    case Nil{}:
194      None{}
195    case Con{Outline.Item{Outline.ICtor{}, name, line, sig, doc, path}, rest}:
196      next_line(rest)
197    case Con{Outline.Item{kind, name, line, sig, doc, path}, rest}:
198      Some{line}
199
200# what the uses name, in order: an item of the file, or one behind an alias
201def called(us: List<&2, Bind.Use>, +as: List<&2, Alias>, +file: String) -> List<&2, Mention>:
202  match us:
203    case Nil{}:
204      Nil{}
205    case Con{Bind.Use{n, l, c, tt}, rest}:
206      target(tt, as, file, called(rest, as, file))
207
208# the names of the top-level defs that are proofs (Calls.proof: one that
209# returns a proof, or one written with no type, which is how Bend fills the
210# law named f). The law states their property, so the coverage rule neither
211# grades them nor walks through them
212def proofs(ds: List<&2, Calls.Def>) -> List<&2, String>:
213  match ds:
214    case Nil{}:
215      Nil{}
216    case Con{Calls.Def{name, +sig, body}, rest}:
217      +more = proofs(rest)
218      Bool.pick(List<&2, String>, Calls.proof(sig), name <> more, more)
219
220# the top-level defs and types, by name and line, less the defs that are
221# proofs (ps): what the coverage rule grades. The uses are read in order,
222# once: each item but a constructor takes those before the next such item's
223# line, and a def or a type keeps what they name (a def's calls)
224def ranged(
225  items: List<&2, Outline.Item>,
226  +us: List<&2, Bind.Use>,
227  +as: List<&2, Alias>,
228  +file: String,
229  +ps: List<&2, String>
230) -> List<&2, Top>:
231  match items:
232    case Nil{}:
233      Nil{}
234    case Con{Outline.Item{Outline.ICtor{}, name, line, sig, doc, path}, rest}:
235      ranged(rest, us, as, file, ps)
236    case Con{Outline.Item{Outline.IDef{}, +name, line, sig, doc, path}, +rest}:
237      +nx = next_line(rest)
238      cs = called(upto(us, nx), as, file)
239      +more = ranged(rest, past(us, nx), as, file, ps)
240      Bool.pick(List<&2, Top>, Imports.has(ps, name), more, Top{Outline.IDef{}, name, line, [], cs} <> more)
241    case Con{Outline.Item{Outline.IType{}, name, line, sig, doc, path}, +rest}:
242      +nx = next_line(rest)
243      Top{Outline.IType{}, name, line, ctors(rest), called(upto(us, nx), as, file)}
244        <> ranged(rest, past(us, nx), as, file, ps)
245    case Con{Outline.Item{kind, name, line, sig, doc, path}, +rest}:
246      ranged(rest, past(us, next_line(rest)), as, file, ps)
247
248# a law file or a test, by its path alone: not under law. Nothing the file's
249# text mentions exempts it; IO is not an exemption, since an IO equality is a
250# law.
251def is_exempt(+pp: String) -> Bool:
252  Bool.or(Paths.is_law_file(pp), Paths.is_test(pp))
253
254# the significant tokens: no space, newline or comment
255def significant(toks: List<&2, Lex.Tok>) -> List<&2, Lex.Tok>:
256  match toks:
257    case Nil{}:
258      Nil{}
259    case Con{Lex.Tok{+k, t, l, c}, rest}:
260      +more = significant(rest)
261      Bool.pick(List<&2, Lex.Tok>, Lex.significant(k), Lex.Tok{k, t, l, c} <> more, more)
262
263# the name after `unsafe def`, when the tokens run so
264def unsafe_name(toks: List<&2, Lex.Tok>) -> Maybe<&2, String>:
265  match toks:
266    case Con{Lex.Tok{k1, +u, l1, c1}, Con{Lex.Tok{k2, +d, l2, c2}, Con{Lex.Tok{k3, n, l3, c3}, rest}}}:
267      Bool.pick(Maybe<&2, String>, Bool.and(String.eq(u, "unsafe"), String.eq(d, "def")), Some{n}, None{})
268    case other:
269      None{}
270
271# an `@` that heads an unsafe def, onto the others
272def push(mm: Maybe<&2, String>, +ll: U32, +cc: U32, more: List<&2, Unsafe>) -> List<&2, Unsafe>:
273  match mm:
274    case None{}:
275      more
276    case Some{n}:
277      Unsafe{n, ll, cc, False{}} <> more
278
279# every `@ unsafe def name` run of tokens, on one line or two
280def unsafe_defs(toks: List<&2, Lex.Tok>) -> List<&2, Unsafe>:
281  match toks:
282    case Nil{}:
283      Nil{}
284    case Con{Lex.Tok{k, +at, +l, +c}, +rest}:
285      +more = unsafe_defs(rest)
286      Bool.pick(List<&2, Unsafe>, String.eq(at, "@"), push(unsafe_name(rest), l, c, more), more)
287
288# a foreign def, when its body is one (Foreign.lanes), onto the others
289def foreign.push(
290  mm: Maybe<&2, Foreign.Lanes>,
291  +name: String,
292  +ll: U32,
293  +cc: U32,
294  more: List<&2, Unsafe>
295) -> List<&2, Unsafe>:
296  match mm:
297    case None{}:
298      more
299    case Some{_lanes}:
300      Unsafe{name, ll, cc, True{}} <> more
301
302# every top-level foreign def, where its header starts
303def foreign_defs(root: Tree.Node) -> List<&2, Unsafe>:
304  match root:
305    case Tree.NCons{Tree.Stmt{Tree.SDef{}, +kids, body}, rest}:
306      foreign.push(Foreign.lanes(body), Foreign.name.of(Bind.declared(kids)), Tree.line(kids), Tree.col(kids),
307        foreign_defs(rest))
308    case Tree.NCons{h, rest}:
309      foreign_defs(rest)
310    case other:
311      Nil{}
312
313# the laws
314# --------
315
316# the doc of the item on a line; the items are in line order, so the search
317# stops at it
318def doc_at(items: List<&2, Outline.Item>, +at: U32) -> String:
319  match items:
320    case Nil{}:
321      ""
322    case Con{Outline.Item{kk, nn, +ln, sig, +doc, pp}, rest}:
323      Lazy.stop(String, U32.is_le(at, ln), Bool.pick(String, U32.is_eq(ln, at), doc, ""), _u => doc_at(rest, at))
324
325# does the first item sit on or after a line (from_line stops there)?
326def from_line.stop(items: List<&2, Outline.Item>, +at: U32) -> Bool:
327  match items:
328    case Nil{}:
329      True{}
330    case Con{Outline.Item{kk, nn, +ln, sig, doc, pp}, rest}:
331      U32.is_le(at, ln)
332
333# the items from the first on or after a line; the items are in line order,
334# and so are a tree's laws, so an item this drops lies above every later law
335# too. stop is from_line.stop of items, carried so the step stays a loop
336def from_line(items: List<&2, Outline.Item>, +at: U32, +stop: Bool) -> List<&2, Outline.Item>:
337  match items:
338    case Nil{}:
339      Nil{}
340    case Con{h, +rest}:
341      match stop:
342        case True{}:
343          h <> rest
344        case False{}:
345          from_line(rest, at, from_line.stop(rest, at))
346
347# every law of a tree, with whether it binds and its doc lines. items is a
348# cursor: each law's search starts where the last one stopped, so the items
349# are read once over all the laws, not once per law
350def laws_of(root: Tree.Node, +items: List<&2, Outline.Item>) -> List<&2, Law>:
351  match root:
352    case Tree.NCons{Tree.Stmt{Tree.SLaw{}, +kids, body}, rest}:
353      +at = Tree.line(kids)
354      +left = from_line(items, at, from_line.stop(items, at))
355      dd = doc_at(left, at)
356      Law{Closed.name.of(Bind.declared(kids)), at, Closed.binds(body), String.lines(dd)} <> laws_of(rest, left)
357    case Tree.NCons{h, rest}:
358      laws_of(rest, items)
359    case other:
360      Nil{}
361
362# the laws of a LAWS.bend; no other file states any
363def laws.in(is_laws: Bool, root: Tree.Node, items: List<&2, Outline.Item>) -> List<&2, Law>:
364  match is_laws:
365    case True{}:
366      laws_of(root, items)
367    case False{}:
368      Nil{}
369
370# the digest
371# ----------
372
373# what a file's laws name: only a LAWS.bend states laws that cover a def
374def stated(is_laws: Bool, root: Tree.Node, bound: Bind.Bound, as: List<&2, Alias>, file: String) -> List<&2, Mention>:
375  match is_laws:
376    case True{}:
377      mentions(uses.of(bound), spans(root), as, file)
378    case False{}:
379      Nil{}
380
381# one parsed file read down to its digest
382def of(s2: Src.Src) -> Digest:
383  Src.Src{+path, text, toks, +tree, +bound, +items} = s2
384  +p = Imports.norm(path)
385  +as = aliases(items, p)
386  Digest{path, p, Imports.dir_of(path), Paths.is_laws(path), Paths.is_law_file(path), is_exempt(p),
387    ranged(items, uses.of(bound), as, p, proofs(Calls.defs(tree))), stated(Paths.is_laws(path), tree, bound, as, p),
388    Imports.targets(items, p),
389    List.append(&2, Unsafe, unsafe_defs(significant(toks)), foreign_defs(tree)),
390    laws.in(Paths.is_laws(path), tree, items)}
391
392# every file the linter read, in one pass, in order
393def all(ss: List<&2, Src.Src>) -> List<&2, Digest>:
394  match ss:
395    case Nil{}:
396      Nil{}
397    case Con{s, rest}:
398      of(s) <> all(rest)
399
400# the import graph of the files, built once for every closure taken over it
401def edges(ds: List<&2, Digest>) -> List<&2, Imports.Edge>:
402  match ds:
403    case Nil{}:
404      Nil{}
405    case Con{Digest{path, norm, dir, is_laws, is_law_file, exempt, tops, says, deps, unsafes, laws}, rest}:
406      Imports.Edge{norm, deps} <> edges(rest)