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
|
(** Source locations. Every form carries one: error messages, the step debugger
|
||||||
and nREPL's find-definition all need them, and retrofitting locations onto a
|
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 = {
|
type t = {
|
||||||
file : string;
|
file : string;
|
||||||
line : int; (* 1-based *)
|
line : int; (* 1-based *)
|
||||||
col : int; (* 1-based *)
|
col : int; (* 1-based *)
|
||||||
|
eline : int; (* 1-based, exclusive end *)
|
||||||
|
ecol : int;
|
||||||
}
|
}
|
||||||
|
|
||||||
let make file line col = { file; line; col }
|
let make file line col = { file; line; col; eline = line; ecol = col }
|
||||||
let unknown = { file = "<unknown>"; line = 0; col = 0 }
|
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
|
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 of_string ~file src = { src; file; pos = 0; line = 1; col = 1 }
|
||||||
|
|
||||||
let here st = Loc.make st.file st.line st.col
|
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 at_end st = st.pos >= String.length st.src
|
||||||
let peek st = if at_end st then '\000' else st.src.[st.pos]
|
let peek st = if at_end st then '\000' else st.src.[st.pos]
|
||||||
let peek2 st =
|
let peek2 st =
|
||||||
@ -99,7 +105,7 @@ let read_string st =
|
|||||||
| c -> advance st; Buffer.add_char buf c; go ()
|
| c -> advance st; Buffer.add_char buf c; go ()
|
||||||
in
|
in
|
||||||
go ();
|
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> *)
|
(* \space \tab \newline \return \nul, or \<any single char> *)
|
||||||
let read_byte st =
|
let read_byte st =
|
||||||
@ -119,7 +125,7 @@ let read_byte st =
|
|||||||
| n when String.length n = 1 -> Char.code n.[0]
|
| n when String.length n = 1 -> Char.code n.[0]
|
||||||
| n -> Loc.fail loc "unknown character literal \\%s" n
|
| n -> Loc.fail loc "unknown character literal \\%s" n
|
||||||
in
|
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. *)
|
(* A token that started with a digit, or with '-'/'+' followed by a digit. *)
|
||||||
let read_number st =
|
let read_number st =
|
||||||
@ -132,26 +138,27 @@ let read_number st =
|
|||||||
in
|
in
|
||||||
if is_hex then
|
if is_hex then
|
||||||
match Int64.of_string_opt text with
|
match Int64.of_string_opt text with
|
||||||
| Some i -> Form.make (Form.Int i) loc
|
| Some i -> spanned st loc (Form.Int i)
|
||||||
| None -> Loc.fail loc "malformed hex literal %s" text
|
| None -> Loc.fail (Loc.upto loc (here st)) "malformed hex literal %s" text
|
||||||
else if String.contains text '.' || String.contains text 'e' then
|
else if String.contains text '.' || String.contains text 'e' then
|
||||||
match float_of_string_opt text with
|
match float_of_string_opt text with
|
||||||
| Some f -> Form.make (Form.Float f) loc
|
| Some f -> spanned st loc (Form.Float f)
|
||||||
| None -> Loc.fail loc "malformed float literal %s" text
|
| None -> Loc.fail (Loc.upto loc (here st)) "malformed float literal %s" text
|
||||||
else
|
else
|
||||||
match Int64.of_string_opt text with
|
match Int64.of_string_opt text with
|
||||||
| Some i -> Form.make (Form.Int i) loc
|
| Some i -> spanned st loc (Form.Int i)
|
||||||
| None -> Loc.fail loc "malformed integer literal %s" text
|
| None -> Loc.fail (Loc.upto loc (here st)) "malformed integer literal %s" text
|
||||||
|
|
||||||
let read_symbol_or_keyword st =
|
let read_symbol_or_keyword st =
|
||||||
let loc = here st in
|
let loc = here st in
|
||||||
let text = take_while st (fun c -> not (is_delimiter c)) 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 = "" then Loc.fail loc "unexpected character %C" (peek st);
|
||||||
if text.[0] = ':' then begin
|
if text.[0] = ':' then begin
|
||||||
if String.length text = 1 then Loc.fail loc "empty keyword";
|
if String.length text = 1 then
|
||||||
Form.make (Form.Kw (String.sub text 1 (String.length text - 1))) loc
|
Loc.fail (Loc.upto loc (here st)) "empty keyword";
|
||||||
|
spanned st loc (Form.Kw (String.sub text 1 (String.length text - 1)))
|
||||||
end else
|
end else
|
||||||
Form.make (Form.Sym text) loc
|
spanned st loc (Form.Sym text)
|
||||||
|
|
||||||
(* ── Forms ─────────────────────────────────────────────────────────── *)
|
(* ── Forms ─────────────────────────────────────────────────────────── *)
|
||||||
|
|
||||||
@ -213,7 +220,7 @@ and read_sugar st loc name = advance st; read_wrapped st loc name
|
|||||||
|
|
||||||
and read_wrapped st loc name =
|
and read_wrapped st loc name =
|
||||||
let inner = read_form st in
|
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 =
|
and read_seq st open_c loc =
|
||||||
advance st;
|
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
|
Loc.fail (here st) "expected %C to close %C, found %C" want open_c c
|
||||||
else go (read_form st :: acc)
|
else go (read_form st :: acc)
|
||||||
in
|
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. *)
|
(** All top-level forms in a source string. *)
|
||||||
let read_all ~file src =
|
let read_all ~file src =
|
||||||
|
|||||||
@ -249,6 +249,38 @@ let () =
|
|||||||
| exception Loc.Error (loc, _) ->
|
| exception Loc.Error (loc, _) ->
|
||||||
check "unclosed reports opening loc" (loc.line = 1 && loc.col = 1));
|
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"
|
if !failures = 0 then print_endline "reader: all tests passed"
|
||||||
else begin
|
else begin
|
||||||
Printf.printf "\n%d failure(s)\n" !failures;
|
Printf.printf "\n%d failure(s)\n" !failures;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user