Tutorial Ltac2 : Ltac2 for Ltac1 Users
Main contributors
- Thomas Lamiaux
Summary
Table of contents
- 1. Introduction
- 1.1 A Brief History of Ltac1
- 1.2 Design Flaws of Ltac1
- 1.3 Ltac2
- 1.4 Status of Ltac2
- 2. Using Ltac2 to Write Proofs
- 2.1 Using Ltac2 in the Ltac1 Proof Mode
- 2.2 Using the Ltac2 Proof Mode
- 3. Ltac2 Is a Proper Functional Programming Language
- 3.1 Types and Type Inference
- 3.2 Call-by-Value Semantics and Thunking
- 3.3 Effects: Printf and References
- 3.4 Exceptions
- 4. Ltac2 as a Meta-Programming Language for Rocq
- 4.1 Foreign Function Interface
- 4.2 Matching Terms and Goals
- 4.3 Quoting and Unquoting
- 4.4 Backtracking
- 4.5 Notations
- 5. Small Case Study
Prerequisites
- Familiarity with Ltac1 and basic Rocq proof writing.
- Ltac2 and its standard library are available by default with Rocq.
1. Introduction
1.1 A Brief History of Ltac1
1.2 Design Flaws of Ltac1
- **No type system.** Ltac1 is completely untyped. Any value can be passed to
any function, and type errors are only caught at runtime, often with cryptic
error messages. This makes writing large libraries and tactics and debugging
very tedious.
- **No data structures.** Ltac1 has no lists, no records, and no algebraic
types. All state must be threaded through the goal or through side channels.
- **Unclear Semantics.** It is hard to predict when a tactic will be
evaluated, or whether a name refers to a Rocq term or an Ltac1 variable.
This leads to subtle and hard-to-diagnose bugs.
- **Limited effects.** Ltac1 lacks support for many basic effects that
are useful in a programming language like printing, or mutable references.
- **Implicit quoting.** The boundary between Gallina (Rocq terms) and Ltac1
meta-programs is not syntactically marked. Ltac1 uses dynamic scoping rules
to resolve names, which are hard to understand and debug.
- **Poor FFI.** Functions from the Rocq codebase are imported all at once, as tactics without types and without any control over what is in scope.
1.3 Ltac2
- It is a proper typed functional programming language of the Hindley–Milner
family, similarly to OCaml, with type inference, algebraic data types,
and a clear call-by-value semantics.
- It has an explicitly typed Foreign Function Interface.
This makes it easy to extend Ltac2 to expose and access primitives like unification,
that were not accessible before, while providing better documentation for it.
As a consequence, it is possible to do more in Ltac2 than in Ltac1.
For instance, it is now possible to manipulate the goal state, and modify
the set of goals under focus etc.
- Quoting and unquoting between Rocq terms (Gallina) and Ltac2 values is now
explicit and syntactically marked. It no longer relies on a hard-to-predict
dynamic decision procedure.
- Backtracking is modelled as streams of possibilities, with fine-grained primitives to manipulate it.
1.4 Status of Ltac2
From Ltac2 Require Import Ltac2 Printf Option.
2. Using Ltac2 to Write Proofs
2.1 Using Ltac2 in the Ltac1 Proof Mode
We can verify the Ltac2 function itself works in a Ltac2 proof:
To call it from a Ltac1 proof script, wrap it with ltac2:(...).
The Ltac2 expression inside must have type unit:
(* set the default mode to Ltac1 *)
Set Default Proof Mode "Classic".
Ltac use_ltac2_in_ltac1 :=
ltac2:(greet_and_close ()).
Goal bool.
Proof.
use_ltac2_in_ltac1.
Qed.
Importantly, ltac2:(...) creates a scope boundary: the code inside is pure
Ltac2, and Ltac1 variables are not in scope there.
For instance, in a Ltac1 function my_intro (id : ident) := ltac2:(intro id),
the id inside ltac2:(...) would be treated as the Ltac2 literal name id,
not as the Ltac1 variable. The resulting tactic would always introduce a
hypothesis named id regardless of what was passed.
To pass Ltac1 values across this boundary, one uses the binder syntax
ltac2:(x1 .. xn |- expr), which explicitly receives Ltac1 values as Ltac2
arguments and binds them under the names x1 .. xn in the Ltac2 scope.
Inside the expression, x1 .. xn have type Ltac1.t and are converted to
typed Ltac2 values using helpers such as Ltac1.to_constr and Ltac1.to_ident.
The Ltac2 wrapper must then be defined as a let-in and applied, due to Ltac1's inner workings.
Set Default Proof Mode "Classic".
Ltac my_exact t :=
let f :=
ltac2:(t |-
let t := Option.get (Ltac1.to_constr t) in
exact $t)
in f t.
Goal bool.
Proof.
my_exact true.
Qed.
2.2 Using the Ltac2 Proof Mode
Goal ∀ P Q R S : Prop, P → Q → R → S → (P ∧ Q) ∧ (R ∧ S).
Proof.
intros P Q R S HP HQ HR HS.
split; split; [exact HP | exact HQ | exact HR | exact HS].
Qed.
In Ltac2, this is now written with the syntax tac1 > [tac21 | tac22] in order
to avoid confusion between chaining tactics and dispatching.
The latter is asymetric and does not compose opposite to ;.
Moreover, tac1; tac2 > [tac31 | tac32]
is now parsed as tac1; (tac2 > [tac31 | tac32]) as Ltac2 no longer
automatically delays tactic execution.
Consequently, if tac1 generates multiple goals, the dispatcher will
attempt to apply the list tac31|tac32 to the subgoals generated by tac2
independently for each goal produced by tac1.
This typically results in an "Incorrect number of goals" error. To achieve
standard Ltac1 factoring, you must use parentheses to explicitly group the
sequence: (tac1; tac2) > foo|bar.
Set Default Proof Mode "Ltac2".
Goal ∀ P Q R S : Prop, P → Q → R → S → (P ∧ Q) ∧ (R ∧ S).
Proof.
intros P Q R S HP HQ HR HS.
Fail split; split > [exact HP | exact HQ | exact HR | exact HS].
(split; split) > [exact HP | exact HQ | exact HR | exact HS].
Qed.
Similarly, some tactic combinators now parse as if they were normal functions.
Parentheses are now required around complex arguments, such as abstractions.
The tacticals affected are: try, repeat, do, once, progress, time, abstract.
For instance, try exact HP is now parsed as (try exact) HP: try receives
exact as its sole argument, and HP is left dangling, causing an error.
It is hence required to write try (exact HP). Respectively for the others.
Set Default Proof Mode "Classic".
Goal ∀ P : Prop, P → P.
Proof.
intros P HP. try exact HP.
Qed.
Set Default Proof Mode "Ltac2".
Goal ∀ P : Prop, P → P.
Proof.
intros P HP.
Fail try exact HP.
try (exact HP).
Qed.
Goal ∀ P : Prop, P → P.
Proof.
intros P HP.
Fail do 1 exact HP.
do 1 (exact HP).
Qed.
However, a real issue with the Ltac2 proof mode is that some functions
are imported but are currently missing notations to handle the quoting
from Rocq to Ltac2.
For instance, in Rocq 9.0, the function clearbody is exposed in
the standard library as:
[Ltac2 @ external clearbody : ident list → unit := "rocq-runtime.plugins.ltac2" "tac_clearbody"]
However, is lacking a notation enabling us to directly write clearbody x y
to clear the body of the local definitions x and y.
You would have to write the quoting yourself clearbody [@x; @y].
In the meantime, there are two main workarounds not to write the quoting yourself.
The first option is to define the missing notation locally.
In this case, one should also consider contributing it upstream to the standard library.
The underlying primitive lives in Std and expects an ident list, so a
notation using the list1(ident) parser -- which parses one or more
space-separated identifiers -- is sufficient.
See the corresponding section for more information.
Ltac2 Notation "clearbody" ids(list1(ident)) := Std.clearbody ids.
Goal ∀ A, A → A × A.
Proof.
intros. pose (x := 2). clearbody x.
Abort.
The second option is to call the tactic through the Ltac1 compatibility
bridge using ltac1:(...).
This is the simplest workaround when you only need the tactic occasionally
and do not want to introduce a local notation, but it comes with the usual
caveats of mixing Ltac1 and Ltac2 (no type checking, limited interoperability
with Ltac2 values).
More generally, any Ltac1 tactic can be embedded into Ltac2 using ltac1:(...).
The resulting Ltac2 expression has type unit and runs the Ltac1 tactic on
the current goal.
However, ltac1:(...) creates a scope boundary: the code inside is pure
Ltac1, and Ltac2 variables are not in scope there. For instance, in a
function my_intro (id : ident) := ltac1:(intro id), the id inside
ltac1:(...) would be treated as the Ltac1 literal name id, not as the
Ltac2 variable -- so the tactic would always introduce a hypothesis named
id regardless of what was passed.
To pass Ltac2 values across this boundary, one uses the binder syntax
ltac1:(x1 .. xn |- tac), which explicitly receives Ltac2 values as Ltac1
arguments and binds them under the names x1 .. xn in the Ltac1 scope.
The resulting expression has type Ltac1.t → .. → Ltac1.t → unit and
must be applied to the Ltac2 values, converted to Ltac1.t using helpers
such as Ltac1.of_constr and Ltac1.of_ident.
Ltac2 my_exact (t : constr) :=
ltac1:(t |- exact t) (Ltac1.of_constr t).
Goal 1 + 1 = 2.
Proof.
my_exact '(eq_refl).
Qed.
Ltac2 my_intro0 (id : ident) :=
ltac1:(id |- intro id) (Ltac1.of_ident id).
Ltac2 Notation "my_intro" id(ident) := my_intro0 id.
Goal ∀ n : nat, n = n.
Proof.
my_intro n. reflexivity.
Qed.
3. Ltac2 Is a Proper Functional Programming Language
3.1 Types and Type Inference
Ltac2 add x y : int := Int.add x y.
Ltac2 Check add.
Ltac2 Eval add 2 3.
Fail Ltac2 Eval add 2 true.
Ltac2 supports Hindley–Milner polymorphism, also called prenex polymorphism.
In prenex polymorphism, type-variable quantifiers must appear at the
outermost level of the type, never nested inside it.
For instance, ∀ 'a, 'a → 'a is a valid polymorphic type: the quantifier
is at the front, and the function works at any type 'a. It is the type of
a function that takes an input of type 'a and returns a value of the same type.
However, ∀ 'a, (∀ 'b, 'b → 'b) → 'a is not valid because 'b is
quantified inside the type. Note that it is not the same as ∀ 'a 'b, ('b → 'b) → 'a.
Ltac2 my_id x := x.
Ltac2 Check my_id.
Ltac2 Eval my_id 42.
Ltac2 Eval my_id true.
Ltac2 provides the following primitive types:
Beyond the built-in types, you can define your own algebraic data
types with Ltac2 Type. As in OCaml, constructor names must start with an
**uppercase** letter (Some, None, S, O, …), while variable and
function names **must** start with a **lowercase** letter.
For instance, a type for arithmetic expressions can be defined by:
- unit: the unit type, with its single value ().
- bool: Booleans, with values true and false.
- int: machine integers (63-bit on a 64-bit platform).
- string: character strings.
- ident: Rocq identifiers (names of hypotheses, variables, …).
- constr: type of Rocq terms in Ltac2.
Ltac2 Type rec expr :=
[ Num(int)
| Add(expr, expr)
| Mul(expr, expr)
].
Fail Ltac2 foo X := X.
Functions can then be defined with the rec keyword for recursion,
and match for pattern-matching similarly to OCaml.
Constructors are then referred to without parentheses, like Add a b.
Ltac2 rec eval_expr (e : expr) : int :=
match e with
| Num n ⇒ n
| Add a b ⇒ Int.add (eval_expr a) (eval_expr b)
| Mul a b ⇒ Int.mul (eval_expr a) (eval_expr b)
end.
(* 1 + 2×3 = 7 *)
Ltac2 Eval eval_expr (Add (Num 1) (Mul (Num 2) (Num 3))).
The standard library provides some of the usual polymorphic types like
list and option, and a few basic functions for them.
Ltac2 Eval [1; 2; 3].
Ltac2 Eval List.map (fun x ⇒ Int.add x 1) [1; 2; 3].
option represents a possibly-absent value: Some x for presence and
None for absence. Here is a function returning the head of a list as an
option, with pattern matching on the constructors of the list type:
Ltac2 safe_head (l : 'a list) : 'a option :=
match l with
| [] ⇒ None
| h :: _ ⇒ Some h
end.
Ltac2 Eval safe_head [1; 2; 3].
Ltac2 Eval safe_head ([] : int list).
3.2 Call-by-Value Semantics and Thunking
Ltac2 bad_ignore (_ : unit) : unit := ().
Passing fail to bad_ignore causes the whole call to fail, because
fail is evaluated before bad_ignore is entered:
The fix is to **thunk** the argument: wrap the tactic in fun () ⇒ ....
A thunk is only evaluated when applied to (), so the callee can decide
when (or whether) to run it.
Ltac2 good_ignore0 (_ : unit → unit) : unit := ().
Goal True.
good_ignore0 (fun () ⇒ fail).
exact I.
Qed.
Writing fun () ⇒ ... at every call site is noisy.
A Ltac2 Notation with the thunk(tactic) parser inserts thunks automatically,
hiding this detail from callers.
For simple abbreviations (no extra parsing), it suffices to declare a notation
that applies the thunked function:
3.3 Effects: Printf and References
3.3.1 Printf
-
i: prints an int -
I: prints an ident -
s: prints a string -
m: prints a message -
t: prints a constr (a Rocq term) -
a: prints a value of any type using a custom formatter fun () x ⇒ ... -
A: same asabut the formatter takes no unit argument -
%: outputs a literal %This makes it much easier to inspect the proof state or debug automation than the idtac approach. For instance, here is a small tactic to print the type of a hypothesis. We will explain the exact syntax in the next section.
Ltac2 print_type0 (h : ident) :=
printf "the type of the hypothesis %I is %t" h (Constr.type (Control.hyp h)).
Ltac2 Notation "print_type" h(ident) := print_type0 h.
Goal nat → bool → True.
Proof.
intros a b.
print_type a.
print_type b.
Abort.
3.3.2 Mutable References
- Ref.ref v: creates a fresh reference initialised to v.
- Ref.get r: returns the current value of r.
- Ref.set r v: replaces the value stored in r with v.
- Ref.update r f: applies f to the current value and stores the result.
Goal ∀ (n m : nat), True.
Proof.
intros n m.
let count := Ref.ref 0 in
clear n; Ref.incr count;
clear m; Ref.incr count;
printf "cleared %i hypotheses" (Ref.get count);
exact I.
Qed.
Note that mutations to a reference are **not rolled back on backtracking**.
If a branch modifies a reference and then fails, the modification persists.
Keep this in mind when combining references with backtracking tactics.
Ltac2 has a built-in type exn for exceptions.
Several exceptions are predefined in the standard library:
The exn type is open: you can add your own variants with the
syntax Ltac2 Type exn ::= [myEx (type)].
3.4 Exceptions
- Tactic_failure (msg : message option) -- the standard tactic failure, raised by most combinators and by fail.
- Out_of_bounds (msg : message option) -- index out of range (e.g. list access).
- Division_by_zero -- integer division by zero.
- Invalid_argument (msg : message option) -- a function received an argument it cannot handle.
- Match_failure -- an inexhaustive pattern match was not satisfied.
Ltac2 Type exn ::= [ OutOfFuel (message option) ].
The easiest method to build a value of type message is to use fprintf,
which works exactly like printf except it returns a message value
instead of printing it.
There are two primitives to raise an exception, with different semantics:
1. Control.throw : exn → 'a -- raises a **non-backtrackable** exception.
It cannot be caught by the backtracking combinators Control.plus or
try. It is meant for programming errors or hard failures where retrying
makes no sense (analogous to a panic).
2. Control.zero : exn → 'a -- raises a **backtrackable** exception.
It signals that the current branch has no solution, which triggers
backtracking: Control.plus will try the alternative branch, and try
will silently recover. See the corresponding section for more information.
Goal False.
Proof.
try (Control.zero (OutOfFuel (Some (fprintf "should succeed and print nothing")))).
Abort.
4. Ltac2 as a Meta-Programming Language for Rocq
4.1 Foreign Function Interface
- Control: interact with the proof state (goal) and backtracking
- Constr: inspect, build, and compare Rocq terms
- Std: reduce terms, call unification, access the environment
- Fresh: to create fresh ident
- Unification: to call unification in a controlled way
- Constr.Unsafe: to access the raw kernel representation of terms
Ltac2 print_hnf_type0 (h : ident) : unit :=
let th := Control.hyp h in
let ty_h := Constr.type th in
let hnf_ty_h := Std.eval_hnf ty_h in
printf "the hnf of the type of %I is %t" h hnf_ty_h.
Ltac2 Notation "print_hnf_type" h(ident) := print_hnf_type0 h.
Goal (let x := 1 in x = 1) → False.
Proof.
intros x. print_hnf_type x.
Abort.
4.2 Matching Terms and Goals
Ltac2 print_all_hyp () :=
match! goal with
| [h : ?t |- _] ⇒ printf "the hypothesis %I has type %t" h t; fail
| [ |- _] ⇒ ()
end.
Goal nat → bool → 0 = 1 → False.
Proof.
intros. print_all_hyp ().
Abort.
Another difference with Ltac1 is that a pattern containing variable bindings
must now be explicit, whereas it used to be optional and dynamically
figured out if not specified. For instance, to match let var := ?expr in
?body, one must write let var := ?expr in @?body var.
Ltac2 print_body_hyp_letin () : unit :=
lazy_match! goal with
| [_ : let var := _ in @?body var |- _] ⇒
printf "the body is expanded as a function: %t" body
end.
Goal ∀ x y : nat, (let a := x + 2 in let b := y + 1 in a = b) → True.
Proof.
intros. print_body_hyp_letin ().
Abort.
4.3 Quoting and Unquoting
Ltac ltac1_close_conj t := split; exact t.
Set Default Proof Mode "Classic".
Goal True ∧ True.
Proof.
ltac1_close_conj I.
Qed.
In Ltac2, every Rocq term must be explicitly **quoted** with ' which
produces a Ltac2 term of type constr, and **unquoted** to recover a Rocq
term using $. Note, for complex implementation reason $ can only be
applied to variables.
If we wanted to rewrite ltac1_close_conj in Ltac2, we would take variable
t : constr as argument, as constr is the only type we can manipulate.
Yet, to apply it to exact which expects an unquoted term, which gives us:
Ltac2 ltac2_close_conj0 (t : constr) := split; exact $t.
Set Default Proof Mode "Ltac2".
To be able to use it with a Rocq value, one then need to quote it to a constr.
For instance, like the following.
The quoting can be done automatically using a notation, we refer to the
following section for further explanations.
Ltac2 Notation "ltac2_close_conj" t(constr) := ltac2_close_conj0 t.
The same applies to Rocq identifiers, which can be created using @.
In Ltac1, you could just write:
Set Default Proof Mode "Classic".
Ltac ltac1_print_hyp_type h :=
let T := type of h in idtac "type:" T.
Goal nat → False.
Proof.
intros H.
ltac1_print_hyp_type H.
Abort.
In Ltac2, hypothesis names have the dedicated type ident.
Write @name to create an ident literal, then use Control.hyp to
recover the corresponding constr:
Set Default Proof Mode "Ltac2".
Ltac2 ltac2_print_hyp_type (h : ident) :=
printf "type: %t" (Constr.type (Control.hyp h)).
Goal nat → False.
Proof.
intros H.
Fail ltac2_print_hyp_type H.
ltac2_print_hyp_type @H.
Abort.
4.4 Backtracking
- match goal (backtracks into branches on failure, etc.),
- Combinators like first [tac1 | tac2 | ...]
- fail n (propagates failure n levels up through match branches)
- Control.zero : exn → 'a -- raises an exception and triggers backtracking.
This is the primitive underlying Ltac2 fail.
- Control.plus : (unit → 'a) → (exn → 'a) → 'a -- stacks a backtracking
choice: try the first thunk; on exception, try the handler.
This is the primitive underlying tac1 + tac2, but it is finer
since different decisions can be performed depending on the exception raised.
- Control.case : (unit → 'a) → ('a × (exn → 'a)) result -- inspects whether a tactic has at least one success.
4.5 Notations
Ltac2 Notation obvious := first [assumption | reflexivity].
Goal 1 = 1 ∧ True.
Proof.
split; obvious.
Qed.
A full Ltac2 Notation declares new parsing rules and keywords which are
specified with "tac_name". Arguments are then given with the syntax
name_arg(X) and X specifies that type of name_arg.
The available argument parsers include:
The basic atoms are:
There also are combinators for optional arguments and list of arguments:
As an example consider reimplementing first using the backtracking operators
as implemented in the Ltac2 standard library.
- tactic / tactic(n) -- parse a tactic at precedence level n (default 5); evaluated eagerly, so use thunk when the argument is a tactic branch that must be delayed.
- thunk(e) -- parse e, then wrap the result in fun () ⇒ .... The most common form is thunk(tactic), which turns each tactic argument into a unit → unit thunk to prevent premature evaluation.
- ident -- parse a plain identifier (type ident).
- constr -- parse a Rocq term (type constr).
- string -- parse a string literal (type string).
- int -- parse an integer literal (type int).
- list0(e) -- parse a whitespace-separated, possibly empty, list of e
- list0(e, "sep") -- same, but with a literal keyword separator sep.
- list1(e) / list1(e, "sep") -- like list0 but require at least one element. The notation my_first [...] below uses list1(thunk(tactic(6)), "|") to parse one or more |-separated tactic branches.
- opt(e) -- parse an optional argument e of type option e
- seq(e1, e2, ...) -- parse a fixed sequence of entries and bind them as a tuple.
Ltac2 rec my_first0 tacs :=
match tacs with
| [] ⇒ Control.zero (Tactic_failure None)
| tac :: tacs ⇒ Control.enter (fun _ ⇒ orelse tac (fun _ ⇒ my_first0 tacs))
end.
To write a notation for it, we write:
The result is that my_first [t1 | t2 | t3] is elaborated to
my_first0 [(fun () ⇒ t1); (fun () ⇒ t2); (fun () ⇒ t3)].
All together, it gives us the notation:
- "my_first" and "" / "" are literal keywords that the parser matches
verbatim; so that my_first [...] is unambiguous
- tacs is the name bound in the body to the parsed argument
- list1(..., "|") parses a non-empty list of elements separated by |.
- tactic(6) parses one tactic branch at precedence level 6.
Level 6 is high enough to accept most compound tactics, yet low enough
that the parser stops at | and ""] instead of consuming them.
- thunk(...) wraps the parsed branch in fun () ⇒ ..., so each branch is turned into a thunk unit → unit. Without thunk, every branch would be executed eagerly -- before my_first0 even runs -- which would defeat the whole purpose of trying alternatives one by one.
Ltac2 Notation "my_first" "[" tacs(list0(thunk(tactic(6)), "|")) "]" := my_first0 tacs.
Goal True.
Proof.
my_first [ (printf "tactic 1"; fail) | (printf "tactic 2"; fail) | exact I ].
Qed.
5. Small Case Study
Ltac simplify_let H :=
let type_h := type of H in
lazymatch type_h with
| let var := ?expr in ?body ⇒
idtac body;
let x := fresh "x" in
set (x := expr) in *;
change ((fun var ⇒ body) x) in H;
lazy head beta in H
end.
Set Default Proof Mode "Classic".
Goal ∀ x y : nat, (let a := x + 2 in let b := y + 1 in a = b) → True.
intros x y h.
Fail simplify_let x.
simplify_let h.
Abort.
In Ltac2, we need to:
In the end, this gives us a script that is similar but with a few
decorations and clearer semantics, which can be written with or without
importing the modules.
- **use lowercase for variables**
- use Control.hyp to recover the body of h
- Constr.type is now a proper function rather than an ad-hoc construction
- use the Fresh module to create fresh variables
- use $ to unquote variables back to Rocq's world
- use @ to create Rocq identifier
Set Default Proof Mode "Ltac2".
Import Control Constr.
Ltac2 simplify_let0 (h : ident) : unit :=
let type_h := type (hyp h) in
lazy_match! type_h with
| let var := ?expr in @?body var ⇒
printf "the body is: %t" body;
let x := Fresh.in_goal @x in
set ($x := $expr) in *;
let x := hyp x in
change ($body $x) in h;
lazy head beta in h
end.
Ltac2 Notation "simplify_let" h(ident) := simplify_let0 h.
Goal ∀ x y : nat, (let a := x + 2 in let b := y + 1 in a = b) → True.
intros x y h.
Fail simplify_let x.
simplify_let h.
Abort.
The advantage of Ltac2 is that the FFI interface enables us to write scripts
we could not have in Ltac1. For instance, we can now use the Constr.Unsafe
API to write the simplify_let tactic by directly accessing the structure
of the term, and performing the substitution by hand rather than relying on
high-level tactics like lazy head beta. This can be seend by
printing the resulting body.
Import Unsafe.
Ltac2 simplify_let_bis0 (h : ident) : unit :=
let type_h := type (hyp h) in
match kind type_h with
| LetIn _ expr body ⇒
printf "the body is: %t" body;
let x := Fresh.in_goal @x in
set ($x := $expr) in *;
let x := hyp x in
let new_body := substnl [x] 0 body in
change ($new_body) in h
| _ ⇒ fail
end.
Ltac2 Notation "simplify_let_bis" h(ident) := simplify_let_bis0 h.
Goal ∀ x y : nat, (let a := x + 2 in let b := y + 1 in a = b) → True.
intros x y h.
Fail simplify_let_bis x.
simplify_let_bis h.
Abort.
Ltac2 simplify_let_bis0 (h : ident) : unit :=
let type_h := type (hyp h) in
match kind type_h with
| LetIn _ expr body ⇒
printf "the body is: %t" body;
let x := Fresh.in_goal @x in
set ($x := $expr) in *;
let x := hyp x in
let new_body := substnl [x] 0 body in
change ($new_body) in h
| _ ⇒ fail
end.
Ltac2 Notation "simplify_let_bis" h(ident) := simplify_let_bis0 h.
Goal ∀ x y : nat, (let a := x + 2 in let b := y + 1 in a = b) → True.
intros x y h.
Fail simplify_let_bis x.
simplify_let_bis h.
Abort.