Summary

October 18, 2016 · View on GitHub

  • Title: Econstrs

  • Drivers: Pierre-Marie Pédrot

  • Status: Draft


Summary

This CEP proposes to give a proper treatment of terms with evars in the ML implementation, by introducing a new module Econstr in the engine folder. Its API would essentially give a way to abstract away from the fact that terms may contain solved evars.

Current situation

As of today, higher layers of Coq use the same representation for terms as the kernel itself, i.e. Constr.t. This datastructure is already hidden under smart constructors and a generic accessor providing an inductive view of a term.

The AST features two tactic-specific nodes, namely Evar and Meta. Most parts of the kernel do not handle these nodes and fail when they encounter it, except notably for reduction. While Meta is deprecated and bound to disappear, Evar is the currently preferred way to use partially defined terms.

An evar is a unique identifier that only makes sense in a given proof state, called an evarmap, which amongst other things maps evars to their typing environment and optional body. Terms featuring evars have an evar normal form (ENF) given a particular environment, built by replacing each defined evar by its body in the term.

In theory, apart for very specific cases, evar-wearing terms should be considered equivalent when they have the same ENF. We call functions that respect the equivalence insensitive. The problem is that most of the functions from the codebase are not insensitive, because they rely on Constr generic accessor to observe the shape of a term.

This has two very annoying consequences:

  • To enforce insensitivity, most functions normalize the term upfront, which is in general a costly operation even though the algorithm is only going to inspect a small part of the term. Evar normalization is indeed a recurring cost in code from the wild, amounting up to 30% of the total time of tactic evaluation.

  • Worse, even when trying to computing the ENF eagerly, some parts of the code fail to be insensitive, leading to very subtle heisenbugs really hard to track down.

Proposed solution

Overview

We advocate for the use of a new type statically ensuring insensitivity, distinct from the Constr.t term and defined outside of the kernel.

Such a type would live in a module Econstr from the engine folder, where the name refers to standard nomenclature for evar-aware tactics. Internally, this would be the same type as Constr.t, but it would be made opaque externally and provided with an accessor that would expand evars on the fly. An API sketch is provided below.

type t
(** This is [Constr.t] internally *)

val of_constr : Constr.t -> t
(** Identity *)

(** Destructor *)

val kind : Evd.evar_map -> t -> (t, t) Constr.kind_of_term
(** Essentially the [Evarutil.kind_of_term_upto] function *)

val to_constr : Evd.evar_map -> t -> Constr.t
(** Essentially the [Evarutil.nf_evar] function *)

(** Constructors *)

(* ... *)

(** Unsafe primitives *)

module Unsafe :
sig

val to_constr : t -> Constr.t
(** Identity *)

end

Pros

  • No more need to normalize upfront, as this is done on-the-fly.
  • Statical guarantee that functions are insensitive.
  • Clear-cut separation between kernel terms and tactic-level terms.

Cons

  • Requires a lot of boilerplate in the transition phase.
  • Requires to duplicate code to handle both plain terms and evar terms
  • Requires to duplicate datastructures embedding terms

Potential solutions to the above issues

Code duplication

The code duplication can be handled through an object-oriented API, thanks to a view argument.

type ('a, 'b) view = {
  view : 'a -> 'b -> ('b, 'b) Constr.kind_of_term;
  make : ('b, 'b) Constr.kind_of_term -> 'b;
}

The 'a argument stands for the state needed to deconstruct a term, and the 'b argument gives the actual term type. An argument of this type would allow to abstract away from the actual implementation of terms. Two typical implementations of view would be:

val constr_view : (unit, Constr.t) view
val econstr_view : (Evd.evar_map, Econstr.t) view

All functions taking terms as arguments should be abstracted in a uniform way. E.g. the function:

val foo : constr -> Id.t -> const list

should be turned into:

val foo : ('a, 'b) view -> 'a -> 'b -> Id.t -> 'b list

This may incur a small overhead, though I believe it would be ridiculous compared to the gain of not computing the ENF upfront. Furthermore, when Coq gets to use a version of OCaml with implicit modules, this could be turned into a much more palatable, and probably more efficient, code.

Type duplication

This issue is more tricky. For opaque types, it is easy to provide the accessors translating freely terms into evar-terms on the fly without having to duplicate the corresponding type. For transparent types mentionning terms, this is more complicated.

Most notably, contexts are defined as lists of algebraic types. I believe that in this case, the kernel structures should be parameterized by the type of terms, and the current kernel variants should be defined as instances of such generic types.