src/lint.bend

bolt@1.26.0.0 · 115 lines · 0 definitions

1# src/lint: the interpreter for `bolt` itself. It starts from a World
2# holding the paths it was given and the working directory
3# (src/lint/world.bend), then loops: it asks
4# the planner (src/lint/plan.bend) what is still open, answers each question
5# by IO, a directory listed through src/walk/ and a file read through
6# src/lsp/files/ (a path src/walk/ calls a directory is answered as one,
7# not read), and adds the answers to the World, until nothing is left to
8# ask. Then it prints the planner's lines and exits with its status. It
9# decides nothing: which files are linted, which bolt.bend grades each
10# finding, and what is printed are the planner's. That it answers and prints
11# faithfully is trusted, not proved: it is kept small enough to review line
12# by line, as is every interpreter here (src/check.bend, src/lsp/server.bend).
13import Base
14import bolt@1.26.0.0/src/lint/world.bend as W
15import bolt@1.26.0.0/src/lint/plan.bend as Plan
16import bolt@1.26.0.0/src/lsp/files/disk.bend as Disk
17import bolt@1.26.0.0/src/walk/disk.bend as WalkDisk
18import bolt@1.26.0.0/src/status.bend as Status
19import bolt@1.26.0.0/src/lazy/lazy.bend as Lazy
20
21# a file's text as an answer
22def read.answer(mm: Maybe<&2, String>) -> W.Answer:
23  match mm:
24    case None{}:
25      W.Unread{}
26    case Some{text}:
27      W.Read{text}
28
29# a path's text as an answer: a directory has none, and a file is read
30def text.answer(dir: Bool, path: String) -> IO(W.Answer):
31  match dir:
32    case True{}:
33      IO.pure(W.Answer, W.Directory{})
34    case False{}:
35      do IO<W.Answer>:
36        mm : Maybe<&2, String> <- Disk.read(path)
37        return read.answer(mm)
38
39# one question, answered
40def answer(ask: W.Ask) -> IO(W.Answer):  # noqa: L001 IO driver
41  match ask:
42    case W.Entries{dir}:
43      do IO<W.Answer>:
44        es : List<&2, String> <- WalkDisk.entries(dir)
45        return W.Listed{es}
46    case W.Contents{+path}:
47      do IO<W.Answer>:
48        dir : Bool <- WalkDisk.is_dir(path)
49        text.answer(dir, path)
50
51# every question, answered, in order
52def answer.all(asks: List<&2, W.Ask>) -> IO(List<&2, W.Reply>):
53  match asks:
54    case Nil{}:
55      IO.pure(List<&2, W.Reply>, [])
56    case Con{+ask, rest}:
57      do IO<List<&2, W.Reply>>:
58        a : W.Answer <- answer(ask)
59        more : List<&2, W.Reply> <- answer.all(rest)
60        return W.Reply{ask, a} <> more
61
62# the World with more answers in it
63def more(world: W.World, rs: List<&2, W.Reply>) -> W.World:
64  W.World{paths, cwd, replies} = world
65  W.World{paths, cwd, List.append(&2, W.Reply, replies, rs)}
66
67# each line printed
68def print_all(lines: List<&2, String>) -> IO(Unit):  # noqa: L001 IO driver
69  match lines:
70    case Nil{}:
71      IO.pure(Unit, Unit{})
72    case Con{l, t}:
73      do IO<Unit>:
74        IO.print(l)
75        print_all(t)
76
77# a plan, carried out: its lines printed, then its exit status
78def exec(pl: Plan.Plan) -> IO(Unit):  # noqa: L001 IO driver
79  Plan.Plan{lines, exit} = pl
80  do IO<Unit>:
81    print_all(lines)
82    Status.stop(U32.is_gt(exit, 0))
83
84# the planner's questions answered and the loop gone round again, or, when
85# there are none, its plan carried out
86def loop.step(asks: List<&2, W.Ask>, +world: W.World, go: W.World -> IO(Unit)) -> IO(Unit):
87  match asks:
88    case Nil{}:
89      exec(Plan.plan(world))
90    case Con{a, rest}:
91      do IO<Unit>:
92        rs : List<&2, W.Reply> <- answer.all(a <> rest)
93        go(more(world, rs))
94
95# the loop, under fuel: the walk's bound, plus a round for the texts and one
96# to plan. Every round adds at least one answer, since the planner never asks
97# what the World answers already.
98def loop(fuel: Nat, +world: W.World) -> IO(Unit):  # noqa: L001 IO driver
99  match fuel:
100    case 0n:
101      IO.die(Unit, 1, "bolt: the lint kept asking")
102    case 1n+f:
103      loop.step(Plan.wants(world), world, ww => loop(f, ww))
104
105# the lint from a working directory; none that can be named is fatal, as no
106# relative path could be resolved against it
107def run.from(paths: List<&2, String>, +cwd: String) -> IO(Unit):
108  Lazy.stop(IO(Unit), String.is_empty(cwd), IO.die(Unit, 1, "bolt: cannot name the working directory"),
109    _u => loop(Nat.add(Plan.limit(), 2n), W.World{paths, cwd, []}))
110
111# the linter, over the files given, or the tree below
112def run(paths: List<&2, String>) -> IO(Unit):  # noqa: L001 IO driver
113  do IO<Unit>:
114    cwd : String <- WalkDisk.cwd()
115    run.from(paths, cwd)