Attention une mise à jour du service Gitlab va être effectuée le mardi 18 janvier (et non lundi 17 comme annoncé précédemment) entre 18h00 et 18h30. Cette mise à jour va générer une interruption du service dont nous ne maîtrisons pas complètement la durée mais qui ne devrait pas excéder quelques minutes.

Commit e71ce45f authored by POTTIER Francois's avatar POTTIER Francois
Browse files

MenhirLib.Engine: introduce a simplified error-handling strategy.

The parameter [strategy] allows choosing between the legacy strategy
and the new one.
parent 85e54666
......@@ -58,6 +58,75 @@ module Make (T : TABLE) = struct
(* ------------------------------------------------------------------------ *)
(* As of 2020/12/16, we introduce a choice between multiple error handling
strategies. *)
(* Regardless of the strategy, when a syntax error is encountered, the
function [initiate] is called, a [HandlingError] checkpoint is produced,
and (after resuming) the function [error] is called. This function checks
whether the current state allows shifting, reducing, or neither, when the
lookahead token is [error]. Its behavior, then, depends on the strategy,
as follows. *)
(* In the legacy strategy, which until now was the only strategy,
- If shifting is possible, then a [Shifting] checkpoint is produced,
whose field [please_discard] is [true], so (after resuming) an
[InputNeeded] checkpoint is produced, and (after a new token
has been provided) the parser leaves error-handling mode and
returns to normal mode.
- If reducing is possible, then one or more reductions are performed.
Default reductions are announced via [AboutToReduce] checkpoints,
whereas ordinary reductions are performed silently. (It is unclear
why this is so.) The parser remains in error-handling mode, so
another [HandlingError] checkpoint is produced, and the function
[error] is called again.
- If neither action is possible and if the stack is nonempty, then a
cell is popped off the stack, then a [HandlingError] checkpoint is
produced, and the function [error] is called again.
- If neither action is possible and if the stack is empty, then the
parse dies with a [Reject] checkpoint. *)
(* The simplified strategy differs from the legacy strategy as follows:
- When shifting, a [Shifting] checkpoint is produced, whose field
[please_discard] is [false], so the parser does not request another
token, and the parser remains in error-handling mode. (If the
destination state of this shift transition has a default reduction,
then the parser will perform this reduction as its next step.)
- When reducing, all reductions are announced by [AboutToReduce]
checkpoints.
- If neither shifting [error] nor reducing on [error] is possible,
then the parser dies with a [Reject] checkpoint. (The parser does
not attempt to pop cells off the stack one by one.)
This simplified strategy is appropriate when the grammar uses the [error]
token in a limited way, where the [error] token always appears at the end
of a production whose semantic action raises an exception (whose purpose
is to signal a syntax error and perhaps produce a custom message). Then,
the parser must not request one token past the syntax error. (In a REPL,
that would be undesirable.) It must perform as many reductions on [error]
as possible, then (if possible) shift the [error] token and move to a new
state where a default reduction will be possible. (Because the [error]
token always appears at the end of a production, no other action can
exist in that state, so a default reduction must exist.) The semantic
action raises an exception, and that is it. *)
(* Let us note that it is also possible to perform no error handling at
all, or to perform customized error handling, by stopping as soon as
the first [ErrorHandling] checkpoint appears. *)
type strategy =
| Legacy
| Simplified
(* ------------------------------------------------------------------------ *)
(* In the code-based back-end, the [run] function is sometimes responsible
for pushing a new cell on the stack. This is motivated by code sharing
concerns. In this interpreter, there is no such concern; [run]'s caller
......@@ -143,8 +212,9 @@ module Make (T : TABLE) = struct
(* Note that, if [please_discard] was true, then we have just called
[discard], so the lookahead token cannot be [error]. *)
(* Returning [HandlingError env] is equivalent to calling [error env]
directly, except it allows the user to regain control. *)
(* Returning [HandlingError env] is like calling [error ~strategy env]
directly, except it allows the user to regain control and choose an
error-handling strategy. *)
if env.error then begin
if log then
......@@ -295,7 +365,7 @@ module Make (T : TABLE) = struct
(* [error] handles errors. *)
and error env =
and error ~strategy env =
assert env.error;
(* Consult the column associated with the [error] pseudo-token in the
......@@ -305,39 +375,64 @@ module Make (T : TABLE) = struct
env.current (* determines a row *)
T.error_terminal (* determines a column *)
T.error_value
error_shift (* shift continuation *)
error_reduce (* reduce continuation *)
error_fail (* failure continuation *)
(error_shift ~strategy) (* shift continuation *)
(error_reduce ~strategy) (* reduce continuation *)
(error_fail ~strategy) (* failure continuation *)
env
and error_shift env please_discard terminal value s' =
(* Here, [terminal] is [T.error_terminal],
and [value] is [T.error_value]. *)
and error_shift ~strategy env please_discard terminal value s' =
assert (terminal = T.error_terminal && value = T.error_value);
(* This state is capable of shifting the [error] token. *)
if log then
Log.handling_error env.current;
(* In the simplified strategy, we change [please_discard] to [false],
which means that we won't request the next token and (therefore)
we will remain in error-handling mode after shifting the [error]
token. *)
let please_discard =
match strategy with Legacy -> please_discard | Simplified -> false
in
shift env please_discard terminal value s'
and error_reduce env prod =
and error_reduce ~strategy env prod =
(* This state is capable of performing a reduction on [error]. *)
if log then
Log.handling_error env.current;
reduce env prod
(* Intentionally calling [reduce] instead of [announce_reduce].
It does not seem very useful, and it could be confusing, to
expose the reduction steps taken during error handling. *)
and error_fail env =
(* In the legacy strategy, we call [reduce] instead of [announce_reduce],
apparently in an attempt to hide the reduction steps performed during
error handling. This seems inconsistent, as the default reduction steps
are still announced. In the simplified strategy, all reductions are
announced. *)
match strategy with
| Legacy ->
reduce env prod
| Simplified ->
announce_reduce env prod
and error_fail ~strategy env =
(* This state is unable to handle errors. In the simplified strategy, we
die immediately. In the legacy strategy, we attempt to pop a stack
cell. (This amounts to forgetting part of what we have just read, in
the hope of reaching a state where we can shift the [error] token and
resume parsing in normal mode. Forgetting past input is not appropriate
when the goal is merely to produce a good syntax error message.) *)
(* This state is unable to handle errors. Attempt to pop a stack
cell. *)
match strategy with
| Simplified ->
Rejected
| Legacy ->
(* Attempt to pop a stack cell. *)
let cell = env.stack in
let next = cell.next in
......@@ -447,9 +542,11 @@ module Make (T : TABLE) = struct
| _ ->
invalid_arg "offer expects InputNeeded"
let resume : 'a . 'a checkpoint -> 'a checkpoint = function
let resume : 'a . ?strategy:strategy -> 'a checkpoint -> 'a checkpoint =
fun ?(strategy=Legacy) checkpoint ->
match checkpoint with
| HandlingError env ->
Obj.magic error env
Obj.magic error ~strategy env
| Shifting (_, env, please_discard) ->
Obj.magic run env please_discard
| AboutToReduce (env, prod) ->
......@@ -493,8 +590,8 @@ module Make (T : TABLE) = struct
All of the cheating resides in the types assigned to [offer] and [handle]
above. *)
let rec loop : 'a . supplier -> 'a checkpoint -> 'a =
fun read checkpoint ->
let rec loop : 'a . ?strategy:strategy -> supplier -> 'a checkpoint -> 'a =
fun ?(strategy=Legacy) read checkpoint ->
match checkpoint with
| InputNeeded _ ->
(* The parser needs a token. Request one from the lexer,
......@@ -502,14 +599,14 @@ module Make (T : TABLE) = struct
checkpoint. Then, repeat. *)
let triple = read() in
let checkpoint = offer checkpoint triple in
loop read checkpoint
loop ~strategy read checkpoint
| Shifting _
| AboutToReduce _
| HandlingError _ ->
(* The parser has suspended itself, but does not need
new input. Just resume the parser. Then, repeat. *)
let checkpoint = resume checkpoint in
loop read checkpoint
let checkpoint = resume ~strategy checkpoint in
loop ~strategy read checkpoint
| Accepted v ->
(* The parser has succeeded and produced a semantic value.
Return this semantic value to the user. *)
......@@ -536,6 +633,8 @@ module Make (T : TABLE) = struct
loop_handle succeed fail read checkpoint
| Shifting _
| AboutToReduce _ ->
(* Which strategy is passed to [resume] here is irrelevant,
since this checkpoint is not [HandlingError _]. *)
let checkpoint = resume checkpoint in
loop_handle succeed fail read checkpoint
| HandlingError _
......@@ -569,6 +668,8 @@ module Make (T : TABLE) = struct
loop_handle_undo succeed fail read (inputneeded, checkpoint)
| Shifting _
| AboutToReduce _ ->
(* Which strategy is passed to [resume] here is irrelevant,
since this checkpoint is not [HandlingError _]. *)
let checkpoint = resume checkpoint in
loop_handle_undo succeed fail read (inputneeded, checkpoint)
| HandlingError _
......@@ -602,6 +703,8 @@ module Make (T : TABLE) = struct
Some env
| AboutToReduce _ ->
(* The parser wishes to reduce. Just follow. *)
(* Which strategy is passed to [resume] here is irrelevant,
since this checkpoint is not [HandlingError _]. *)
shifts (resume checkpoint)
| HandlingError _ ->
(* The parser fails, which means it rejects the terminal symbol
......
......@@ -78,9 +78,9 @@ module type INCREMENTAL_ENGINE = sig
| Rejected
(* [offer] allows the user to resume the parser after it has suspended
itself with a checkpoint of the form [InputNeeded env]. [offer] expects the
old checkpoint as well as a new token and produces a new checkpoint. It does not
raise any exception. *)
itself with a checkpoint of the form [InputNeeded env]. [offer] expects
the old checkpoint as well as a new token and produces a new checkpoint.
It does not raise any exception. *)
val offer:
'a checkpoint ->
......@@ -89,10 +89,31 @@ module type INCREMENTAL_ENGINE = sig
(* [resume] allows the user to resume the parser after it has suspended
itself with a checkpoint of the form [AboutToReduce (env, prod)] or
[HandlingError env]. [resume] expects the old checkpoint and produces a new
checkpoint. It does not raise any exception. *)
[HandlingError env]. [resume] expects the old checkpoint and produces a
new checkpoint. It does not raise any exception. *)
(* The optional argument [strategy] influences the manner in which [resume]
deals with checkpoints of the form [ErrorHandling _]. Its default value
is [Legacy]. It can be briefly described as follows:
- If the [error] token is used only to report errors (that is, if the
[error] token appears only at the end of a production, whose semantic
action raises an exception) then the simplified strategy should be
preferred. (This includes the case where the [error] token does not
appear at all in the grammar.)
- If the [error] token is used to recover after an error, or if
perfect backward compatibility is required, the legacy strategy
should be selected.
More details on these strategies appear in the file [Engine.ml]. *)
type strategy =
| Legacy
| Simplified
val resume:
?strategy:strategy ->
'a checkpoint ->
'a checkpoint
......@@ -102,7 +123,8 @@ module type INCREMENTAL_ENGINE = sig
type supplier =
unit -> token * position * position
(* A pair of a lexer and a lexing buffer can be easily turned into a supplier. *)
(* A pair of a lexer and a lexing buffer can be easily turned into a
supplier. *)
val lexer_lexbuf_to_supplier:
(Lexing.lexbuf -> token) ->
......@@ -117,9 +139,11 @@ module type INCREMENTAL_ENGINE = sig
(* [loop supplier checkpoint] begins parsing from [checkpoint], reading
tokens from [supplier]. It continues parsing until it reaches a
checkpoint of the form [Accepted v] or [Rejected]. In the former case, it
returns [v]. In the latter case, it raises the exception [Error]. *)
returns [v]. In the latter case, it raises the exception [Error].
The optional argument [strategy], whose default value is [Legacy],
is passed to [resume] and influences the error-handling strategy. *)
val loop: supplier -> 'a checkpoint -> 'a
val loop: ?strategy:strategy -> supplier -> 'a checkpoint -> 'a
(* [loop_handle succeed fail supplier checkpoint] begins parsing from
[checkpoint], reading tokens from [supplier]. It continues parsing until
......@@ -128,10 +152,10 @@ module type INCREMENTAL_ENGINE = sig
observed first). In the former case, it calls [succeed v]. In the latter
case, it calls [fail] with this checkpoint. It cannot raise [Error].
This means that Menhir's traditional error-handling procedure (which pops
the stack until a state that can act on the [error] token is found) does
not get a chance to run. Instead, the user can implement her own error
handling code, in the [fail] continuation. *)
This means that Menhir's error-handling procedure does not get a chance
to run. For this reason, there is no [strategy] parameter. Instead, the
user can implement her own error handling code, in the [fail]
continuation. *)
val loop_handle:
('a -> 'answer) ->
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment