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