A location is a span, because a column cannot draw a squiggle
Loc.t grows an exclusive end, defaulting to the start, so a location nobody widened is a zero-width span at a point and every existing call site keeps its old meaning. Only the reader knows where a form ends, so only the reader fills them in — one helper in the one place that holds both ends, which is why nothing above Reader had to learn a span exists. The width assertion is the point of the tests: the field could exist, nothing could fill it, and every underline would be one character long while the feature looked finished.
This commit is contained in:
parent
54027ca942
commit
3295f2f640
48
lib/loc.ml
48
lib/loc.ml
@ -1,15 +1,51 @@
|
||||
(** Source locations. Every form carries one: error messages, the step debugger
|
||||
and nREPL's find-definition all need them, and retrofitting locations onto a
|
||||
reader is far worse than carrying them from the start. *)
|
||||
reader is far worse than carrying them from the start.
|
||||
|
||||
A location is a *span*, not a point. The start is what [file:line:col]
|
||||
prints and what every consumer that wants one place uses; the end is what
|
||||
lets an error underline the thing it is about. A column number cannot draw
|
||||
a squiggle and a span can, which is the whole reason the two extra fields
|
||||
are here.
|
||||
|
||||
The end is *exclusive* and defaults to the start, so a location nobody
|
||||
widened is a zero-width span at a point and every old call site keeps its
|
||||
old meaning. Only the reader knows where a form ends, so only the reader
|
||||
fills these in; a location the checker invents for a node with no syntax
|
||||
stays a point. *)
|
||||
|
||||
type t = {
|
||||
file : string;
|
||||
line : int; (* 1-based *)
|
||||
col : int; (* 1-based *)
|
||||
file : string;
|
||||
line : int; (* 1-based *)
|
||||
col : int; (* 1-based *)
|
||||
eline : int; (* 1-based, exclusive end *)
|
||||
ecol : int;
|
||||
}
|
||||
|
||||
let make file line col = { file; line; col }
|
||||
let unknown = { file = "<unknown>"; line = 0; col = 0 }
|
||||
let make file line col = { file; line; col; eline = line; ecol = col }
|
||||
let unknown = make "<unknown>" 0 0
|
||||
|
||||
(** [upto start stop] is [start] widened to end where [stop] begins. A [stop]
|
||||
that is not after [start], or is in another file, leaves it alone: a span
|
||||
that runs backwards would draw nonsense. *)
|
||||
let upto (start : t) (stop : t) =
|
||||
if
|
||||
stop.file = start.file
|
||||
&& (stop.line > start.line
|
||||
|| (stop.line = start.line && stop.col > start.col))
|
||||
then { start with eline = stop.line; ecol = stop.col }
|
||||
else start
|
||||
|
||||
(** True when the span covers more than its first line. The underline is drawn
|
||||
on the start line either way — a form that spans twenty lines is pointed
|
||||
at, not boxed — so this is what tells the renderer to run the underline to
|
||||
the end of that line rather than to [ecol]. *)
|
||||
let multiline (t : t) = t.eline > t.line
|
||||
|
||||
(** How many columns to underline on the start line, or [None] when the span
|
||||
was never widened and there is nothing but a point to draw. *)
|
||||
let width (t : t) =
|
||||
if t.eline = t.line && t.ecol > t.col then Some (t.ecol - t.col) else None
|
||||
|
||||
let to_string t = Printf.sprintf "%s:%d:%d" t.file t.line t.col
|
||||
|
||||
|
||||
@ -39,6 +39,12 @@ type state = {
|
||||
let of_string ~file src = { src; file; pos = 0; line = 1; col = 1 }
|
||||
|
||||
let here st = Loc.make st.file st.line st.col
|
||||
|
||||
(* A form's location is the span it occupies, so every reader below takes the
|
||||
location it started at and closes it where the cursor now is. Doing it here,
|
||||
in the one place that knows both ends, is why nothing above [Reader] has to
|
||||
know a span exists. *)
|
||||
let spanned st loc v = Form.make v (Loc.upto loc (here st))
|
||||
let at_end st = st.pos >= String.length st.src
|
||||
let peek st = if at_end st then '\000' else st.src.[st.pos]
|
||||
let peek2 st =
|
||||
@ -99,7 +105,7 @@ let read_string st =
|
||||
| c -> advance st; Buffer.add_char buf c; go ()
|
||||
in
|
||||
go ();
|
||||
Form.make (Form.Str (Buffer.contents buf)) loc
|
||||
spanned st loc (Form.Str (Buffer.contents buf))
|
||||
|
||||
(* \space \tab \newline \return \nul, or \<any single char> *)
|
||||
let read_byte st =
|
||||
@ -119,7 +125,7 @@ let read_byte st =
|
||||
| n when String.length n = 1 -> Char.code n.[0]
|
||||
| n -> Loc.fail loc "unknown character literal \\%s" n
|
||||
in
|
||||
Form.make (Form.Byte code) loc
|
||||
spanned st loc (Form.Byte code)
|
||||
|
||||
(* A token that started with a digit, or with '-'/'+' followed by a digit. *)
|
||||
let read_number st =
|
||||
@ -132,26 +138,27 @@ let read_number st =
|
||||
in
|
||||
if is_hex then
|
||||
match Int64.of_string_opt text with
|
||||
| Some i -> Form.make (Form.Int i) loc
|
||||
| None -> Loc.fail loc "malformed hex literal %s" text
|
||||
| Some i -> spanned st loc (Form.Int i)
|
||||
| None -> Loc.fail (Loc.upto loc (here st)) "malformed hex literal %s" text
|
||||
else if String.contains text '.' || String.contains text 'e' then
|
||||
match float_of_string_opt text with
|
||||
| Some f -> Form.make (Form.Float f) loc
|
||||
| None -> Loc.fail loc "malformed float literal %s" text
|
||||
| Some f -> spanned st loc (Form.Float f)
|
||||
| None -> Loc.fail (Loc.upto loc (here st)) "malformed float literal %s" text
|
||||
else
|
||||
match Int64.of_string_opt text with
|
||||
| Some i -> Form.make (Form.Int i) loc
|
||||
| None -> Loc.fail loc "malformed integer literal %s" text
|
||||
| Some i -> spanned st loc (Form.Int i)
|
||||
| None -> Loc.fail (Loc.upto loc (here st)) "malformed integer literal %s" text
|
||||
|
||||
let read_symbol_or_keyword st =
|
||||
let loc = here st in
|
||||
let text = take_while st (fun c -> not (is_delimiter c)) in
|
||||
if text = "" then Loc.fail loc "unexpected character %C" (peek st);
|
||||
if text.[0] = ':' then begin
|
||||
if String.length text = 1 then Loc.fail loc "empty keyword";
|
||||
Form.make (Form.Kw (String.sub text 1 (String.length text - 1))) loc
|
||||
if String.length text = 1 then
|
||||
Loc.fail (Loc.upto loc (here st)) "empty keyword";
|
||||
spanned st loc (Form.Kw (String.sub text 1 (String.length text - 1)))
|
||||
end else
|
||||
Form.make (Form.Sym text) loc
|
||||
spanned st loc (Form.Sym text)
|
||||
|
||||
(* ── Forms ─────────────────────────────────────────────────────────── *)
|
||||
|
||||
@ -213,7 +220,7 @@ and read_sugar st loc name = advance st; read_wrapped st loc name
|
||||
|
||||
and read_wrapped st loc name =
|
||||
let inner = read_form st in
|
||||
Form.make (Form.List [ Form.make (Form.Sym name) loc; inner ]) loc
|
||||
spanned st loc (Form.List [ Form.make (Form.Sym name) loc; inner ])
|
||||
|
||||
and read_seq st open_c loc =
|
||||
advance st;
|
||||
@ -232,7 +239,8 @@ and read_seq st open_c loc =
|
||||
Loc.fail (here st) "expected %C to close %C, found %C" want open_c c
|
||||
else go (read_form st :: acc)
|
||||
in
|
||||
Form.make (wrap open_c (go [])) loc
|
||||
let items = go [] in
|
||||
spanned st loc (wrap open_c items)
|
||||
|
||||
(** All top-level forms in a source string. *)
|
||||
let read_all ~file src =
|
||||
|
||||
@ -249,6 +249,38 @@ let () =
|
||||
| exception Loc.Error (loc, _) ->
|
||||
check "unclosed reports opening loc" (loc.line = 1 && loc.col = 1));
|
||||
|
||||
(* ── Spans ─────────────────────────────────────────────────────
|
||||
A location ends where the form ends, which is what an underline needs
|
||||
and what a column number cannot give. Asserted on the width rather than
|
||||
on the end column alone: a span that never got widened is zero wide, and
|
||||
that is the failure mode worth catching — the field would exist, nothing
|
||||
would fill it, and every squiggle would be one character long. *)
|
||||
(match read ~file:"f.flan" "(foo bar)" with
|
||||
| [ l ] ->
|
||||
check "span covers the list" (Loc.width l.loc = Some 9);
|
||||
(match l.Form.v with
|
||||
| Form.List [ head; arg ] ->
|
||||
check "span covers the head symbol" (Loc.width head.loc = Some 3);
|
||||
check "span covers the argument" (Loc.width arg.loc = Some 3);
|
||||
check "span starts at the symbol" (arg.loc.col = 6)
|
||||
| _ -> check "span: two elements" false)
|
||||
| _ -> check "span: one form" false);
|
||||
|
||||
(match read ~file:"f.flan" "\"hi\" 42 :kw" with
|
||||
| [ s; n; k ] ->
|
||||
check "span covers a string with its quotes" (Loc.width s.loc = Some 4);
|
||||
check "span covers a number" (Loc.width n.loc = Some 2);
|
||||
check "span covers a keyword with its colon" (Loc.width k.loc = Some 3)
|
||||
| _ -> check "span: three atoms" false);
|
||||
|
||||
(* A form that runs over a line end has no width on its first line, and says
|
||||
so rather than reporting a negative one. *)
|
||||
(match read ~file:"f.flan" "(a\n b)" with
|
||||
| [ l ] ->
|
||||
check "multi-line span is flagged" (Loc.multiline l.loc);
|
||||
check "multi-line span has no single-line width" (Loc.width l.loc = None)
|
||||
| _ -> check "span: one multi-line form" false);
|
||||
|
||||
if !failures = 0 then print_endline "reader: all tests passed"
|
||||
else begin
|
||||
Printf.printf "\n%d failure(s)\n" !failures;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user