src/lsp/report.bend

bolt@1.26.0.0 · 170 lines · 0 definitions

1# lsp/report: the checker's text, as diagnostics. bend prints an error as
2#   Error:            (or `Error: a one-line message`)
3#   - expected : ..   the message lines, then `Context:` and its lines
4#   Location: name    bend's key for the def: `name` in this file, the
5#                     import's namespace first in another (`m.g`, `../m.g`)
6#   4>|   x           the source excerpt; `>|` marks the line, 1-based
7# There are no columns, so a diagnostic covers its whole line. An error in an
8# import cannot be placed in this file: it lands on line 0, naming its home.
9# Which errors are this file's is read against the file's own keys (mine).
10#
11# Trusted, not proved: `bend <file> --check-only` never runs `main`, and
12# prints its report in the shape read here, naming a def in `Location:` the
13# way checker/names.bend builds its names. bend is a separate program, and the
14# format has drifted once already (`report_import`).
15import Base
16import bolt@1.26.0.0/src/lazy/lazy.bend as Lazy
17
18# line is 0-based, as LSP counts
19type Diag is Data:
20  Diag{line: U32, msg: String}
21
22# a line of the checker's report, by what it starts with
23type Line is Data:
24  KError{rest: String}
25  KLocation{place: String}
26  KOther{text: String}
27
28# an error bend reported, before it is placed: where it is (the Location's
29# name, bend's key for its def), the line it marks (1-based, 0 for none), and
30# its message
31type Raw is Data:
32  Raw{place: String, line: U32, body: String}
33
34# msg and out are reversed
35type St is Data:
36  St{seen: Bool, located: Bool, msg: List<&2, String>, line: U32, place: String, out: List<&2, Raw>}
37
38# a report line's kind
39def classify(+ss: String) -> Line:
40  Bool.pick(Line, String.starts_with(ss, "Error:"), KError{String.drop(ss, 6n)},
41    Bool.pick(Line, String.starts_with(ss, "Location:"), KLocation{String.drop(ss, 9n)}, KOther{ss}))
42
43# reading a marker past its padding: `count` digits of `whole` so far, and at
44# the first other character, the number they make when `>|` follows them
45def marker.go(ss: String, +whole: String, +count: Nat) -> Maybe<&2, U32>:
46  match ss:
47    case SNil{}:
48      None{}
49    case SCon{+hh, +tt}:
50      Lazy.stop(Maybe<&2, U32>, Bool.not(Char.is_digit(hh)),
51        Bool.pick(Maybe<&2, U32>, String.starts_with(SCon{hh, tt}, ">|"), U32.read(String.take(whole, count)), None{}),
52        _u => marker.go(tt, whole, 1n+count))
53
54# the line number a source line of the report carries, when it does: `12>|
55# code` marks line 12, padding before the digits aside; a line of context
56# (`11 | code`) marks none, whatever its code holds
57def marker(ss: String) -> Maybe<&2, U32>:
58  +tt = String.trim_start(ss)
59  marker.go(tt, tt, 0n)
60
61# a message line, without its `- `
62def clean(+ss: String) -> String:
63  Bool.pick(String, String.starts_with(ss, "- "), String.drop(ss, 2n), ss)
64
65# bend's "N is available: run bend update" is not part of a diagnostic
66def is_nag(+ss: String) -> Bool:
67  String.contains(ss, "is available: run bend update")
68
69# a non-empty, non-nag line onto the message
70def push(+ss: String, +msg: List<&2, String>) -> List<&2, String>:
71  Bool.pick(List<&2, String>, Bool.or(String.is_empty(ss), is_nag(ss)), msg, ss <> msg)
72
73# the error so far, its message lines reversed, onto the ones before it; "no
74# main to run" is a library, not an error
75def flush(seen: Bool, msg: List<&2, String>, +line: U32, +place: String, +out: List<&2, Raw>) -> List<&2, Raw>:
76  +body = String.join(List.reverse(&2, String, msg), "\n")
77  Bool.pick(List<&2, Raw>, Bool.and(seen, Bool.not(String.eq(body, "no main to run"))),
78    Raw{place, line, body} <> out, out)
79
80def step.go(
81  kk: Line,
82  located: Bool,
83  seen: Bool,
84  msg: List<&2, String>,
85  +line: U32,
86  place: String,
87  out: List<&2, Raw>
88) -> St:
89  match kk located seen:
90    case KError{rest} l s:
91      St{True{}, False{}, push(String.trim_start(rest), []), 0, "", flush(s, msg, line, place, out)}
92    case KLocation{p} l s:
93      St{s, True{}, msg, line, String.trim_start(p), out}
94    case KOther{text} False{} True{}:
95      St{True{}, False{}, push(clean(text), msg), line, place, out}
96    case KOther{text} True{} True{}:
97      St{True{}, True{}, msg, Maybe.default(&2, U32, marker(text), line), place, out}
98    case KOther{text} l False{}:
99      St{False{}, l, msg, line, place, out}
100
101# one report line
102def step(ss: String, st: St) -> St:
103  St{seen, located, msg, line, place, out} = st
104  step.go(classify(ss), located, seen, msg, line, place, out)
105
106# every report line, in order
107def run(lines: List<&2, String>, st: St) -> St:
108  match lines:
109    case Nil{}:
110      st
111    case Con{l, t}:
112      run(t, step(l, st))
113
114# the errors once the lines are over, a pending one flushed
115def finish(st: St) -> List<&2, Raw>:
116  St{seen, located, msg, line, place, out} = st
117  List.reverse(&2, Raw, flush(seen, msg, line, place, out))
118
119# the text before the first `~`: an instance of a template (`f~1`) is the
120# template's
121def stem(ss: String) -> String:
122  match ss:
123    case SNil{}:
124      SNil{}
125    case SCon{+hh, tt}:
126      Lazy.stop(String, Char.is_eq(hh, '~'), SNil{}, _u => SCon{hh, stem(tt)})
127
128# is the name among these?
129def owns(names: List<&2, String>, +name: String) -> Bool:
130  match names:
131    case Nil{}:
132      False{}
133    case Con{hh, tt}:
134      Lazy.or_else(String.eq(hh, name), _u => owns(tt, name))
135
136# is an error at this place in the checked file? With the file's names
137# (bend's keys for its items, checker/names.bend): when the place is one of
138# them or names nothing. Without them: when the place holds no path. bend
139# names a def of an import beside the file with no path (`m.g`), so only
140# the names tell it from a def `m.g` of the file
141def mine(names: Maybe<&2, List<&2, String>>, +place: String) -> Bool:
142  match names:
143    case None{}:
144      Bool.not(String.contains(place, "/"))
145    case Some{ks}:
146      Bool.or(String.is_empty(place), owns(ks, stem(place)))
147
148# an error placed: on its marked line, 0-based, in the checked file; on line
149# 0, its message headed by where it is, in another file
150def diag.at(+own: Bool, +place: String, +line: U32, +body: String) -> Diag:
151  Diag{Bool.pick(U32, Bool.or(Bool.not(own), U32.is_eq(line, 0)), 0, (line - 1 : U32)),
152    Bool.pick(String, own, body, "in " ++ place ++ ":\n" ++ body)}
153
154# an error placed, by the checked file's names
155def diag(names: Maybe<&2, List<&2, String>>, rr: Raw) -> Diag:
156  Raw{+place, line, body} = rr
157  diag.at(mine(names, place), place, line, body)
158
159# every error placed
160def diags(rs: List<&2, Raw>, +names: Maybe<&2, List<&2, String>>) -> List<&2, Diag>:
161  match rs:
162    case Nil{}:
163      []
164    case Con{rr, rest}:
165      diag(names, rr) <> diags(rest, names)
166
167# the checker's text as diagnostics, placed by the checked file's names when
168# they are known
169def parse_in(+names: Maybe<&2, List<&2, String>>, text: String) -> List<&2, Diag>:
170  diags(finish(run(String.lines(text), St{False{}, False{}, [], 0, "", []})), names)