Skip to content

Commit ff355c8

Browse files
committed
REPL: Better error messages and other utilities
Using the LSP file buffers introduced previously we can implement a 'file' Sail_file.interactive_repl that corresponds to the commands entered into the REPL. The error formatting code can then use this for better errors in the REPL. Add an :edit command that opens an editor to provide longer commands
1 parent 387771d commit ff355c8

File tree

9 files changed

+255
-90
lines changed

9 files changed

+255
-90
lines changed

src/bin/repl.ml

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ open Ast_defs
7272
open Ast_util
7373
open Interpreter
7474
open Pretty_print_sail
75+
open Reporting.Position
7576

7677
module Callgraph_commands = Callgraph_commands
7778

@@ -141,7 +142,8 @@ let prompt istate =
141142
)
142143
(IdSet.elements istate.display_options.registers)
143144
);
144-
match istate.mode with Normal -> "sail> " | Evaluation _ -> "eval> "
145+
let l = Sail_file.repl_prompt_line () in
146+
match istate.mode with Normal -> Printf.sprintf "REPL:%d> " l | Evaluation _ -> Printf.sprintf "REPL:%d eval> " l
145147

146148
let mode_clear istate =
147149
match istate.mode with
@@ -417,7 +419,23 @@ let help =
417419
(color green ":help :help")
418420
)
419421

420-
type input = Command of string * string | Expression of string | Empty
422+
type input = Command of string * string * Lexing.position | Expression of string * Lexing.position | Empty
423+
424+
let editor = ref "vim"
425+
426+
let editor_command cmd =
427+
let open Lexing in
428+
let temp_file = Filename.temp_file "repl" ".sail" in
429+
Reporting.system_checked (!editor ^ " " ^ temp_file);
430+
let contents = Util.read_whole_file temp_file in
431+
let start_line, start_bol = Sail_file.add_to_repl_contents ~command:contents in
432+
let pos = { pos_fname = "REPL"; pos_lnum = start_line; pos_bol = start_bol; pos_cnum = start_bol } in
433+
if cmd = "" then Expression (contents, pos) else Command (cmd, contents, pos)
434+
435+
let () =
436+
let open Interactive in
437+
ArgString ("command", fun cmd -> ActionUnit (fun _ -> editor := cmd))
438+
|> register_command ~name:"set_editor" ~help:"Set the editor for the :edit command. Default vim"
421439

422440
(* This function is called on every line of input passed to the interpreter *)
423441
let handle_input' istate input =
@@ -426,16 +444,22 @@ let handle_input' istate input =
426444
(* Process the input and check if it's a command, a raw expression,
427445
or empty. *)
428446
let input =
447+
let open Lexing in
429448
if input <> "" && input.[0] = ':' then (
449+
let start_line, start_bol = Sail_file.add_to_repl_contents ~command:input in
430450
let n = try String.index input ' ' with Not_found -> String.length input in
431451
let cmd = Str.string_before input n in
432-
let arg = String.trim (Str.string_after input n) in
433-
Command (cmd, arg)
452+
let arg = Str.string_after input n in
453+
let pos = { pos_fname = "REPL"; pos_lnum = start_line; pos_bol = start_bol; pos_cnum = start_bol + n } in
454+
Command (cmd, String.trim arg, trim_position arg pos)
434455
)
435456
else if String.length input >= 2 && input.[0] = '/' && input.[1] = '/' then
436457
(* Treat anything starting with // as a comment *)
437458
Empty
438-
else if input <> "" then Expression input
459+
else if input <> "" then (
460+
let start_line, start_bol = Sail_file.add_to_repl_contents ~command:input in
461+
Expression (input, { pos_fname = "REPL"; pos_lnum = start_line; pos_bol = start_bol; pos_cnum = start_bol })
462+
)
439463
else Empty
440464
in
441465

@@ -446,10 +470,12 @@ let handle_input' istate input =
446470
istate
447471
in
448472

473+
let input = match input with Command (":edit", arg, pos) -> editor_command arg | input -> input in
474+
449475
(* First handle commands that are mode-independent *)
450476
let istate =
451477
match input with
452-
| Command (cmd, arg) -> begin
478+
| Command (cmd, arg, pos) -> begin
453479
match cmd with
454480
| ":n" | ":normal" -> { istate with mode = Normal }
455481
| ":t" | ":type" ->
@@ -461,17 +487,17 @@ let handle_input' istate input =
461487
Value.output_close ();
462488
exit 0
463489
| ":i" | ":infer" ->
464-
let exp = Initial_check.exp_of_string arg in
490+
let exp = Initial_check.exp_of_string ~inline:pos arg in
465491
let exp = Type_check.infer_exp istate.env exp in
466492
Document.to_channel stdout (doc_typ (Type_check.typ_of exp));
467493
print_newline ();
468494
istate
469495
| ":prove" ->
470-
let nc = Initial_check.constraint_of_string arg in
496+
let nc = Initial_check.constraint_of_string ~inline:pos arg in
471497
print_endline (string_of_bool (Type_check.prove __POS__ istate.env nc));
472498
istate
473499
| ":assume" ->
474-
let nc = Initial_check.constraint_of_string arg in
500+
let nc = Initial_check.constraint_of_string ~inline:pos arg in
475501
{ istate with env = Type_check.Env.add_constraint nc istate.env }
476502
| ":v" | ":verbose" ->
477503
Type_check.set_tc_debug (int_of_string arg);
@@ -491,8 +517,7 @@ let handle_input' istate input =
491517
[
492518
"Universal commands - :(t)ype :(i)nfer :(q)uit :(v)erbose :prove :assume :clear :commands :help \
493519
:output :option :show_register :hide_register";
494-
"Normal mode commands - :elf :(l)oad :(u)nload :let :def :(b)ind :recheck :compile :reset "
495-
^ more_commands;
520+
"Normal mode commands - :elf :let :def :(b)ind :recheck :compile :reset " ^ more_commands;
496521
"Evaluation mode commands - :(r)un :(s)tep :step_(f)unction :(n)ormal";
497522
"";
498523
":(c)ommand can be called as either :c or :command.";
@@ -573,36 +598,44 @@ let handle_input' istate input =
573598
match istate.mode with
574599
| Normal -> begin
575600
match input with
576-
| Command (cmd, arg) -> begin
601+
| Command (cmd, arg, pos) -> begin
577602
(* Normal mode commands *)
578603
match cmd with
579-
| ":b" | ":bind" ->
580-
let args = Str.split (Str.regexp " +") arg in
581-
begin
582-
match args with
583-
| v :: ":" :: args ->
584-
let typ = Initial_check.typ_of_string (String.concat " " args) in
585-
let _, env, _ = Type_check.bind_pat istate.env (mk_pat (P_id (mk_id v))) typ in
586-
{ istate with env }
587-
| _ -> failwith "Invalid arguments for :bind"
588-
end
589-
| ":let" ->
590-
let args = Str.split (Str.regexp " +") arg in
591-
begin
592-
match args with
593-
| v :: "=" :: args ->
594-
let exp = Initial_check.exp_of_string (String.concat " " args) in
595-
let defs, env =
596-
Type_check.check_defs istate.env [mk_def (DEF_let (mk_letbind (mk_pat (P_id (mk_id v))) exp)) ()]
597-
in
598-
{ istate with ast = append_ast_defs istate.ast defs; env }
599-
| _ -> failwith "Invalid arguments for :let"
600-
end
604+
| ":b" | ":bind" -> begin
605+
match Util.split_on_char ':' arg with
606+
| [v; arg] ->
607+
let typ = Initial_check.typ_of_string ~inline:(advance_position ~after:1 ~trim:false v pos) arg in
608+
let v_l = string_location ~start:pos ~trim:true v in
609+
let _, env, _ =
610+
Type_check.bind_pat istate.env (mk_pat ~loc:v_l (P_id (mk_id ~loc:v_l (String.trim v)))) typ
611+
in
612+
{ istate with env }
613+
| _ -> failwith "Invalid arguments for :bind"
614+
end
615+
| ":let" -> begin
616+
match Util.split_on_char '=' arg with
617+
| [v; exp_str] ->
618+
let exp = Initial_check.exp_of_string ~inline:(advance_position ~after:1 ~trim:false v pos) exp_str in
619+
let arg_l = string_location ~start:pos ~trim:true arg in
620+
let v_l = string_location ~start:pos ~trim:true v in
621+
let defs, env =
622+
Type_check.check_defs istate.env
623+
[
624+
mk_def ~loc:arg_l
625+
(DEF_let (mk_letbind ~loc:arg_l (mk_pat ~loc:v_l (P_id (mk_id ~loc:v_l (String.trim v)))) exp))
626+
();
627+
]
628+
in
629+
{ istate with ast = append_ast_defs istate.ast defs; env }
630+
| _ -> failwith "Invalid arguments for :let"
631+
end
601632
| ":def" ->
633+
(* Add an extra blank line so we can handle directives that require a newline to be parsed. *)
634+
ignore (Sail_file.add_to_repl_contents ~command:"");
602635
let ast =
603-
Initial_check.ast_of_def_string_with __POS__
636+
Initial_check.ast_of_def_string_with ~inline:pos __POS__
604637
(Preprocess.preprocess istate.default_sail_dir None istate.options)
605-
arg
638+
(arg ^ "\n")
606639
in
607640
let ast, env = Type_check.check istate.env ast in
608641
{ istate with ast = append_ast istate.ast ast; env }
@@ -652,10 +685,10 @@ let handle_input' istate input =
652685
| None -> unrecognised_command istate cmd
653686
)
654687
end
655-
| Expression str ->
688+
| Expression (str, pos) ->
656689
(* An expression in normal mode is type checked, then puts
657690
us in evaluation mode. *)
658-
let exp = Type_check.infer_exp istate.env (Initial_check.exp_of_string str) in
691+
let exp = Type_check.infer_exp istate.env (Initial_check.exp_of_string ~inline:pos str) in
659692
let istate = setup_interpreter_state istate in
660693
let istate = { istate with mode = Evaluation (eval_frame (Step (lazy "", istate.state, return exp, []))) } in
661694
print_program istate;
@@ -664,7 +697,7 @@ let handle_input' istate input =
664697
end
665698
| Evaluation frame -> begin
666699
match input with
667-
| Command (cmd, arg) -> begin
700+
| Command (cmd, arg, pos) -> begin
668701
(* Evaluation mode commands *)
669702
match cmd with
670703
| ":r" | ":run" -> run istate
@@ -720,8 +753,9 @@ let handle_input istate input =
720753
| Failure str ->
721754
print_endline ("Error: " ^ str);
722755
istate
723-
| Type_error.Type_error (_, err) ->
724-
print_endline (fst (Type_error.string_of_type_error err));
756+
| Type_error.Type_error (l, err) ->
757+
let msg, hint = Type_error.string_of_type_error err in
758+
Reporting.print_type_error ?hint l msg;
725759
istate
726760
| Reporting.Fatal_error err ->
727761
Reporting.print_error ~interactive:true err;

src/lib/ast_util.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ let rec is_gen_loc = function
202202
| Parse_ast.Hint (_, l1, l2) -> is_gen_loc l1 || is_gen_loc l2
203203
| Parse_ast.Range _ -> false
204204

205-
let mk_id str = Id_aux (Id str, Parse_ast.Unknown)
205+
let mk_id ?loc:(l = Parse_ast.Unknown) str = Id_aux (Id str, l)
206206

207207
let mk_nc nc_aux = NC_aux (nc_aux, Parse_ast.Unknown)
208208

@@ -248,7 +248,7 @@ let mk_fundef funcls =
248248
let rec_opt = Rec_aux (Rec_nonrec, Parse_ast.Unknown) in
249249
DEF_aux (DEF_fundef (FD_aux (FD_function (rec_opt, tannot_opt, funcls), no_annot)), mk_def_annot Parse_ast.Unknown ())
250250

251-
let mk_letbind pat exp = LB_aux (LB_val (pat, exp), no_annot)
251+
let mk_letbind ?loc:(l = Parse_ast.Unknown) pat exp = LB_aux (LB_val (pat, exp), (l, empty_uannot))
252252

253253
let mk_val_spec vs_aux = DEF_aux (DEF_val (VS_aux (vs_aux, no_annot)), mk_def_annot Parse_ast.Unknown ())
254254

src/lib/ast_util.mli

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ val is_order_dec : order -> bool
176176

177177
(** {2 Functions for building untyped AST elements} *)
178178

179-
val mk_id : string -> id
179+
val mk_id : ?loc:l -> string -> id
180180
val mk_kid : string -> kid
181181
val mk_nc : n_constraint_aux -> n_constraint
182182
val mk_nexp : nexp_aux -> nexp
@@ -199,7 +199,7 @@ val mk_qi_id : kind_aux -> kid -> quant_item
199199
val mk_qi_nc : n_constraint -> quant_item
200200
val mk_qi_kopt : kinded_id -> quant_item
201201
val mk_fexp : id -> uannot exp -> uannot fexp
202-
val mk_letbind : uannot pat -> uannot exp -> uannot letbind
202+
val mk_letbind : ?loc:l -> uannot pat -> uannot exp -> uannot letbind
203203
val mk_kopt : ?loc:l -> kind_aux -> kid -> kinded_id
204204
val mk_def : ?loc:l -> ('a, 'b) def_aux -> 'b -> ('a, 'b) def
205205

src/lib/initial_check.ml

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,31 +1474,49 @@ let initial_ctx =
14741474
target_sets = StringMap.empty;
14751475
}
14761476

1477-
let exp_of_string str =
1478-
try
1479-
let exp = Parser.exp_eof (Lexer.token (ref [])) (Lexing.from_string str) in
1480-
to_ast_exp initial_ctx exp
1481-
with Parser.Error -> Reporting.unreachable Parse_ast.Unknown __POS__ ("Failed to parse " ^ str)
1477+
let inline_lexbuf lexbuf inline =
1478+
(* Note that OCaml >= 4.11 has a much less hacky way of doing this *)
1479+
let open Lexing in
1480+
match inline with
1481+
| Some p ->
1482+
lexbuf.lex_curr_p <- p;
1483+
lexbuf.lex_abs_pos <- p.pos_cnum
1484+
| None -> ()
14821485

1483-
let typschm_of_string str =
1486+
let parse_from_string action ?inline str =
1487+
let lexbuf = Lexing.from_string str in
14841488
try
1485-
let typschm = Parser.typschm_eof (Lexer.token (ref [])) (Lexing.from_string str) in
1486-
let typschm, _ = to_ast_typschm initial_ctx typschm in
1487-
typschm
1488-
with Parser.Error -> Reporting.unreachable Parse_ast.Unknown __POS__ ("Failed to parse " ^ str)
1489+
inline_lexbuf lexbuf inline;
1490+
action lexbuf
1491+
with Parser.Error ->
1492+
let pos = Lexing.lexeme_start_p lexbuf in
1493+
let tok = Lexing.lexeme lexbuf in
1494+
raise (Reporting.err_syntax pos (Printf.sprintf "Failed to parse '%s' at token '%s'" str tok))
14891495

1490-
let typ_of_string str =
1491-
try
1492-
let typ = Parser.typ_eof (Lexer.token (ref [])) (Lexing.from_string str) in
1493-
let typ = to_ast_typ initial_ctx typ in
1494-
typ
1495-
with Parser.Error -> Reporting.unreachable Parse_ast.Unknown __POS__ ("Failed to parse " ^ str)
1496+
let exp_of_string =
1497+
parse_from_string (fun lexbuf ->
1498+
let exp = Parser.exp_eof (Lexer.token (ref [])) lexbuf in
1499+
to_ast_exp initial_ctx exp
1500+
)
14961501

1497-
let constraint_of_string str =
1498-
try
1499-
let atyp = Parser.typ_eof (Lexer.token (ref [])) (Lexing.from_string str) in
1500-
to_ast_constraint initial_ctx atyp
1501-
with Parser.Error -> Reporting.unreachable Parse_ast.Unknown __POS__ ("Failed to parse " ^ str)
1502+
let typschm_of_string =
1503+
parse_from_string (fun lexbuf ->
1504+
let typschm = Parser.typschm_eof (Lexer.token (ref [])) lexbuf in
1505+
let typschm, _ = to_ast_typschm initial_ctx typschm in
1506+
typschm
1507+
)
1508+
1509+
let typ_of_string =
1510+
parse_from_string (fun lexbuf ->
1511+
let typ = Parser.typ_eof (Lexer.token (ref [])) lexbuf in
1512+
to_ast_typ initial_ctx typ
1513+
)
1514+
1515+
let constraint_of_string =
1516+
parse_from_string (fun lexbuf ->
1517+
let atyp = Parser.typ_eof (Lexer.token (ref [])) lexbuf in
1518+
to_ast_constraint initial_ctx atyp
1519+
)
15021520

15031521
let extern_of_string ?(pure = false) id str =
15041522
VS_val_spec (typschm_of_string str, id, Some { pure; bindings = [("_", string_of_id id)] }) |> mk_val_spec
@@ -1700,17 +1718,24 @@ let process_ast ?(generate = true) ast =
17001718
}
17011719
else ast
17021720

1703-
let ast_of_def_string_with ocaml_pos f str =
1721+
let ast_of_def_string_with ?inline ocaml_pos f str =
17041722
let lexbuf = Lexing.from_string str in
1723+
lexbuf.lex_curr_p <- { pos_fname = ""; pos_lnum = 1; pos_bol = 0; pos_cnum = 0 };
1724+
inline_lexbuf lexbuf inline;
17051725
let internal = !opt_magic_hash in
17061726
opt_magic_hash := true;
1707-
lexbuf.lex_curr_p <- { pos_fname = ""; pos_lnum = 1; pos_bol = 0; pos_cnum = 0 };
1708-
let def = Parser.def_eof (Lexer.token (ref [])) lexbuf in
1727+
let def =
1728+
try Parser.def_eof (Lexer.token (ref [])) lexbuf
1729+
with Parser.Error ->
1730+
let pos = Lexing.lexeme_start_p lexbuf in
1731+
let tok = Lexing.lexeme lexbuf in
1732+
raise (Reporting.err_syntax pos ("current token: " ^ tok))
1733+
in
17091734
let ast = Reporting.forbid_errors ocaml_pos (fun ast -> process_ast ~generate:false ast) (P.Defs [("", f [def])]) in
17101735
opt_magic_hash := internal;
17111736
ast
17121737

1713-
let ast_of_def_string ocaml_pos str = ast_of_def_string_with ocaml_pos (fun x -> x) str
1738+
let ast_of_def_string ?inline ocaml_pos str = ast_of_def_string_with ?inline ocaml_pos (fun x -> x) str
17141739

17151740
let defs_of_string ocaml_pos str = (ast_of_def_string ocaml_pos str).defs
17161741

@@ -1753,18 +1778,12 @@ let parse_project ?inline ?filename:f ~contents:s () =
17531778
let open Project in
17541779
let open Lexing in
17551780
let lexbuf = from_string s in
1756-
1757-
(* Note that OCaml >= 4.11 has a much less hacky way of doing this *)
1758-
begin
1759-
match inline with
1760-
| Some p ->
1761-
lexbuf.lex_curr_p <- p;
1762-
lexbuf.lex_abs_pos <- p.pos_cnum
1763-
| None -> lexbuf.lex_curr_p <- { pos_fname = Option.get f; pos_lnum = 1; pos_bol = 0; pos_cnum = 0 }
1764-
end;
1781+
if Option.is_none inline then
1782+
lexbuf.lex_curr_p <- { pos_fname = Option.get f; pos_lnum = 1; pos_bol = 0; pos_cnum = 0 };
1783+
inline_lexbuf lexbuf inline;
17651784

17661785
try Project_parser.file Project_lexer.token lexbuf
17671786
with Project_parser.Error ->
1768-
let pos = Lexing.lexeme_start_p lexbuf in
1769-
let tok = Lexing.lexeme lexbuf in
1787+
let pos = lexeme_start_p lexbuf in
1788+
let tok = lexeme lexbuf in
17701789
raise (Reporting.err_syntax pos ("current token: " ^ tok))

0 commit comments

Comments
 (0)