src/lint.bend
bolt@1.23.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.23.0.0/src/lint/world.bend as W
15import bolt@1.23.0.0/src/lint/plan.bend as Plan
16import bolt@1.23.0.0/src/lsp/files/disk.bend as Disk
17import bolt@1.23.0.0/src/walk/disk.bend as WalkDisk
18import bolt@1.23.0.0/src/status.bend as Status
19import bolt@1.23.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)