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 --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`). 15 import Base 16 import ../lazy/lazy.bend as Lazy 17 18 # line is 0-based, as LSP counts 19 type Diag is Data: 20 Diag{line: U32, msg: String} 21 22 # a line of the checker's report, by what it starts with 23 type 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 31 type Raw is Data: 32 Raw{place: String, line: U32, body: String} 33 34 # msg and out are reversed 35 type 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 39 def 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 45 def 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 57 def 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 `- ` 62 def 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 66 def 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 70 def 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 75 def 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 80 def 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 102 def 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 107 def 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 115 def 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 121 def 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? 129 def 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 141 def 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 150 def 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 155 def 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 160 def 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 169 def 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)