package stele
Source file parser_error_runtime.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160module type ENGINE = sig type 'a env type element val get : int -> 'a env -> element option val positions : element -> Lexing.position * Lexing.position end module Make (E : ENGINE) = struct type label = { loc_start : Lexing.position; loc_end : Lexing.position; text : string; } module R = MenhirLib.ErrorReports (* A source slice for a Menhir [$i] reference: extract, sanitize, compress whitespace, and cap the width (max 43 chars, as [E.shorten 20] gives). *) let show source positions = R.extract source positions |> R.sanitize |> R.compress |> R.shorten 20 let resolve ~source ~env message = (* Expand Menhir [$i] source-slice references first (a no-op for a message with none), reaching stack cell [i] via the engine. *) let get_slice i = match E.get i env with | Some el -> show source (E.positions el) | None -> "???" in let message = R.expand get_slice message in let lines = String.split_on_char '\n' message in let = ref [] in let main_message = ref [] in List.iter (fun line -> let len = String.length line in if len > 2 && line.[0] = '<' then try let i = String.index line '>' in let tag = String.sub line 1 (i - 1) in let msg = String.trim (String.sub line (i + 1) (len - i - 1)) in (* Two marker kinds share the [<…>text] shape. A bare number [<N>] is a delimiter hint: underline the single opening delimiter of the construct at stack cell [N]. A caret-tagged number [<^N>] is the *subject* of a hedge ("Assuming that the X is complete, …"): underline the whole construct that cell [N] produced. A consumer that only understands [<N>] fails [int_of_string "^N"] and leaves the line inline in the message — graceful degradation. *) let is_subject = String.length tag > 0 && tag.[0] = '^' in (* The depth is plain decimal digits: [<+2>] or [<0x2>] is not a marker, and stays in the message. *) let digits = if is_subject then String.sub tag 1 (String.length tag - 1) else tag in if digits = "" || not (String.for_all (fun c -> c >= '0' && c <= '9') digits) then raise Exit; let depth = int_of_string digits in match E.get (depth - 1) env with | Some el -> let pos1, pos2 = E.positions el in if is_subject then if (* The subject label spans the construct's true start..end, however many source lines it crosses — the diagnostic renderer draws a multi-line span as a spine, so the whole construct is underlined. An empty (epsilon) reduction — "the exports are complete" with zero exports assumed complete — leaves a zero-width span; underlining nothing is noise, so drop the label and let the hedge sentence stand on its own. *) pos1.Lexing.pos_cnum >= pos2.Lexing.pos_cnum then () else related := { loc_start = pos1; loc_end = pos2; text = msg } :: !related else (* This hint points at an opening delimiter. The delimiter is normally the cell's start — a compound opener ([(then]/[(param]/…) reports the '(' as its start — but a spurious reduction can surface a plain token (e.g. ELEM) sitting just past the '('; in that case walk the source back over blanks, line breaks included, to the delimiter. The underline spans the FULL alias the label names, not a fixed single character: a multi-character opener like [\[|] underlines both of its characters. The width is read from the alias the label quotes ([This '[|' opens …] -> 2, [This '(' opens …] -> 1), so the [<N>] marker itself carries no width and stays interchangeable with older output; a label with no quoted alias falls back to one character. *) let cnum = pos1.Lexing.pos_cnum in let is_delim c = c = '(' || c = '[' || c = '{' in let blank c = c = ' ' || c = '\t' || c = '\n' || c = '\r' in let dcnum = if cnum < String.length source && is_delim source.[cnum] then cnum else let rec back i = if i < 0 || not (blank source.[i]) then if i >= 0 && is_delim source.[i] then i else cnum else back (i - 1) in back (cnum - 1) in let width = match String.index_opt msg '\'' with | Some i -> ( match String.index_from_opt msg (i + 1) '\'' with | Some j -> max 1 (j - i - 1) | None -> 1) | None -> 1 in (* When the walk crossed line breaks, move the line number and the line start back with it. *) let start = let crossed = ref 0 and bol = ref pos1.Lexing.pos_bol in for i = dcnum to cnum - 1 do if source.[i] = '\n' then incr crossed done; if !crossed > 0 then begin bol := 0; for i = 0 to dcnum - 1 do if source.[i] = '\n' then bol := i + 1 done end; { pos1 with Lexing.pos_cnum = dcnum; pos_lnum = pos1.Lexing.pos_lnum - !crossed; pos_bol = !bol; } in related := { loc_start = start; loc_end = { start with Lexing.pos_cnum = dcnum + width }; text = msg; } :: !related | None -> main_message := line :: !main_message with _ -> main_message := line :: !main_message else main_message := line :: !main_message) lines; let main_message = List.rev !main_message in let = List.rev !related in (* Remove a trailing empty line left by a trailing newline when there are related labels. *) let main_message = match List.rev main_message with | "" :: rest when related_labels <> [] -> List.rev rest | _ -> main_message in (String.concat "\n" main_message, related_labels) end