Evar-insensitive terms were introduced in 8.7, following CEP #10. We will not recap the motivations in this document and rather summarize the code changes to perform.
The essential datastructures are defined in
the EConstr
module module. It defines
the tactic counterparts of kernel data structures such as terms
(EConstr.constr
), universes (EConstr.ESorts.t
) and contexts
(EConstr.*_context
).
The main difference with kernel-side types is that observing them requires
an evar-map at hand in order to normalize evars on the fly. The basic primitive
to observe an EConstr.t
is the following function:
val kind : Evd.evar_map -> t -> (t, t, ESorts.t, EInstance.t) Constr.kind_of_term
(** Same as {!Constr.kind} except that it expands evars and normalizes
universes on the fly. *)
Essentially, each time it sees an evar which happens to be defined in the provided evar-map, it replaces it with the corresponding body and carries on.
Due to universe unification occurring at the tactic level, the same goes for
universe instances and sorts. See the ESort
and EInstance
modules in
EConstr
.
This normalization is critical for the soundness of tactics. Before EConstr, a lot of bugs were lurking in the code base, a few still are (most notably in meta-based unification) and failure to respect the guidelines thereafter may result in nasal demons.
As a rule of thumb, all functions living at the tactic level should manipulate
EConstr.t
instead of Constr.t
, and similarly for the other data structures.
To ease the transition, the EConstr
module defines a handful of aliases to
shadow the type names from the kernel.
It is recommended to perform the following replacement in headers.
(** Kernel types. You may remove the two following opens if you want. Beware
that [kind_of_term] needs to be in scope if you use [EConstr.kind] so that
you may still need to open one of the two. *)
open Term
open Constr
(** Tactic types. Open this after to shadow kernel types. *)
open EConstr
Note that the EConstr
module also redefines a Vars
submodule.
All functions deconstructing an econstr need to take an evar-map as a parameter. Therefore, you need to pass one as an argument virtually everywhere.
In the Coq source code, it is recommended to take the evar-map as a first
argument called sigma
, except if the function also takes an environment in
which case it is passed second. Namely, the two typical instances are:
let foo sigma c = mycode
val foo : Evd.evar_map -> EConstr.t -> Foo.t
let bar env sigma c = mycode
val bar : Environ.env -> Evd.evar_map -> EConstr.t -> Bar.t
The EConstr API makes the code much more sensitive to evar-maps, because a
lot of now useless normalizations were removed. Thus one should be cautious of
not dropping the evar-map when it has been updated, and should rather stick
to a strict state-passing discipline. Unsound primitives like
Typing.unsafe_type_of
are also a known source of problems, so you should
replace them with the corresponding evar-map-returning function and thread it
properly.
Many functions from Constr
and Term
are redefined to work on econstr in
the EConstr
module, so that it is often enough to perform the open
as
described above to replace them. Their type may differ though, because they now
need access to an evar-map. A lot of econstr-manipulating functions are also
defined in Termops
.
Functions manipulating tactic terms and kernel terms share the same name if they
are the equivalent one of the other. Do not hesitate to grep Coq mli files to
find the equivalent of a function you want to port if it is neither in EConstr
nor in Termops
(this should be very rare).
Sometimes you do not have any other choice than calling kernel-side functions on terms, and conversely to turn a kernel term into a tactic term.
There are two functions to do so.
EConstr.of_constr
turns kernel terms into tactic terms. It is currently the physical identity, and thus O(1), but this may change in the future.EConstr.to_constr
turns tactic terms into kernel terms. It performs a full-blown normalization of the given term, which is O(n) and potentially costly.
For performance reasons, avoiding to jump back and forth between kernel and tactic terms is recommended.
There are also a few unsafe conversion functions that take advantage of the
fact that EConstr.t
is internally the same as Constr.t
. Namely,
EConstr.Unsafe.to_constr
is the physical identity. It should not be used
in typical code and is instead provided for efficiency when you know what you
are doing. Either use it to reimplement low-level functions that happen to
be insensitive externally, or use it to provide backward compatibility with
broken code that relies on evar-sensitivity. Do not use it because it is
easier than stuffing evar-maps everywhere. You've been warned.
The EConstr branch fixed a lot of eisenbugs linked to lack of normalization everywhere, most notably in unification. It may also have introduced a few, so if you see a change in behaviour that looks like a bug, please report it. Obviously, unification is not specified, so it's hard to tell apart, but still.
Efficiency has been affected as well. We now pay an overhead when observing a term, but at the same time a lot of costly upfront normalizations were removed.