Skip to content

Commit 206fa34

Browse files
committed
More edits
Co-authored-by : Yaël Dillies <[email protected]>
1 parent 52924e9 commit 206fa34

File tree

1 file changed

+85
-54
lines changed

1 file changed

+85
-54
lines changed

posts/simp-made-simple.md

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ slug: simp-made-simple
99
tags: 'simp, simproc, meta'
1010
title: Simp, made simple.
1111
type: text
12+
header-includes:
13+
- \usepackage{tikz}
1214
---
1315

1416
This is the second blog post in a series of three.
@@ -60,8 +62,32 @@ Roughly speaking, when traversing an expression `e`, `simp` does the following i
6062

6163
We call it the *simplification loop*.
6264

65+
```mermaid
66+
graph TD
67+
%% Define nodes with explicit positioning
68+
e["e"]
69+
e1["e₁"]
70+
e2["e₂"]
71+
72+
%% Downward arrows with "pre" labels
73+
e -->|pre| e1
74+
e -->|pre| e2
75+
76+
%% Upward arrows with "post" labels
77+
e1 -->|post| e
78+
e2 -->|post| e
79+
```
80+
81+
In the figure above, the simplification loop does the following:
82+
1. Preprocedures on `e`
83+
2. Preprocedures on `e₁`
84+
3. Postprocedures on `e₁` (as it has no children)
85+
4. Preprocedures on `e₂`
86+
5. Postprocedures on `e₂` (as it has no children)
87+
6. Postprocedures on `e` (as it has no further children)
88+
6389
The above loop is merely an approximation of the true simplification loop:
64-
Each procedure actually gets to decide whether to go to step 1 or 3 after it was triggered,
90+
each procedure actually gets to decide whether to go to step 1 or 3 after it was triggered,
6591
as we shall see in the coming subsection.
6692

6793
## `Step`
@@ -75,39 +101,35 @@ This is encapsulated by the [`Result`](https://leanprover-community.github.io/ma
75101
structure:
76102
```lean
77103
structure Result where
78-
expr : Expr -- The new expression `e'`
79-
proof? : Option Expr := none -- The proof that `e = e'`
104+
/-- The new expression `e'` -/
105+
expr : Expr
106+
/-- The proof that `e = e'` -/
107+
proof? : Option Expr := none
80108
-- Note, `Result` currently has an extra `cache` field that is both deprecated
81109
-- and irrelevant to our current discussion.
82110
```
83-
Note that the proof is optional:
84-
If `proof?` is set to its default `none` value, the equality is assumed to be definitional.
85-
Also note that the proof is allowed to be an arbitrary `Expr`, i.e. nothing ensures that it actually is a proof that `e = e'`.
86-
87-
The location to be simplified next has only three options:
88-
1. Simplify again the same expression is very limited given as a choice
89-
1. Simplify an expression `e` to a new expression `e'` and stop there (i.e. don't visit any subexpressions in the case of a preprocedure)
90-
2. Simplify an expression `e` to a new expression `e'` and continuing the process *at* `e'` (i.e. `e'` may be simplified further), before moving to subexpressions if this is a preprocedure.
91-
3. Simplify an expression `e` to a new expression `e'` and continue the process *on subexpressions* of `e'` (if this is a preprocedure).
92-
93-
This is used as follows: if a procedure simplified an expression `e` to a new expression `e'` and `p` is a proof that `e = e'` then we capture this by `⟨e', p⟩ : Result`.
94-
If `e` and `e'` are definitionally equal, one can in fact omit the `proof?` term.
95-
96-
The type `Step` has three constructors, which correspond to the three types of actions:
111+
> The proof is optional:
112+
> If `proof?` is set to its default `none` value, the equality is assumed to be definitional.
113+
114+
> The proof is allowed to be an arbitrary `Expr`,
115+
> i.e. nothing ensures that it actually is a proof that `e = e'`.
116+
> It is up to the author of the procedure to make sure that the generated proof terms are valid.
117+
118+
After simplifying the current expression `e` to a new expression `e'`,
119+
there are a few possible options for the next location:
120+
1. Simplify `e'` further.
121+
Preprocedures are tried on `e'`.
122+
2. Simplify subexpressions of `e'`.
123+
Preprocedures are tried on each child expression of `e'` in turn.
124+
If `e'` has no child, then we run postprocedures on `e'`.
125+
3. Don't simplify further.
126+
Preprocedures are tried on the next child of the parent expression.
127+
If there is no such child, then postprocedures are tried on the parent expression.
128+
129+
The three possibilities above correspond to the three constructors of `Step`:
97130
```lean
98131
inductive Step where
99-
/--
100-
For `pre` procedures, it returns the result without visiting any subexpressions.
101-
102-
For `post` procedures, it returns the result.
103-
-/
104-
| done (r : Result)
105-
/--
106-
For `pre` procedures, the resulting expression is passed to `pre` again.
107-
108-
For `post` procedures, the resulting expression is passed to `pre` again IF
109-
`Simp.Config.singlePass := false` and resulting expression is not equal to initial expression.
110-
-/
132+
/-- Try preprocedures on the simplified expression. -/
111133
| visit (e : Result)
112134
/--
113135
For `pre` procedures, continue transformation by visiting subexpressions, and then
@@ -116,36 +138,47 @@ inductive Step where
116138
For `post` procedures, this is equivalent to returning `visit`.
117139
-/
118140
| continue (e? : Option Result := none)
141+
/-- Returns the result without visiting any subexpressions. -/
142+
| done (r : Result)
119143
```
144+
> If a procedure fails to simplify an expression, it should return `continue none`.
145+
Both `visit` and `done` signify success.
146+
120147

121-
At any given point, we can do three things:
122-
1. Simplify an expression `e` to a new expression `e'` and stop there (i.e. don't visit any subexpressions in the case of a preprocedure)
123-
2. Simplify an expression `e` to a new expression `e'` and continuing the process *at* `e'` (i.e. `e'` may be simplified further), before moving to subexpressions if this is a preprocedure.
124-
3. Simplify an expression `e` to a new expression `e'` and continue the process *on subexpressions* of `e'` (if this is a preprocedure).
125-
126-
Note that the 2 and 3 are the same for `post` procedures.
127148

128149
Whenever a simproc is called on a given expression, it outputs a `Step`, which determines what will happen next during the `simp` call.
129-
One important point worth remembering is that since every simproc call is running a metaprogram to produce the output `Step`, the constructor that ends up used may vary according to the input.
150+
Since every simproc call is running a metaprogram to produce the output `Step`, the constructor that ends up used may vary according to the input.
130151
For example, a given simproc may in some cases use `visit` and in others use `continue`.
131152

132153
To make this more concrete, let's take a look at of each of these use cases in action.
133154

134-
- `done`. Recall from the first post the simproc `Nat.reduceDvd`.
155+
- `done`.
156+
Recall from the first post the simproc `Nat.reduceDvd`.
135157
This takes expressions of the form `a | b` where `a`, `b` are explicit natural numbers, and returns `True` or `False`.
136158
Either way, the output is in simp normal form, so there is no need to attempt to simplify it further.
137-
Thus, this simproc uses `done` to output the result of this simplification.
138-
- `visit`. Let's consider the simproc `reduceIte` (also in the first post!).
139-
This takes expressions of the form `if h then a else b` and outputs `a` (resp. `b`) if `h` can be simplified to `True` (resp. `False`). Since `a` and `b` could be arbitrarily complicated expressions, it makes sense to try and simplify them further, so this simproc uses `visit` to output the result of this simplification.
159+
Thus, the simproc uses `done` to this result.
160+
- `visit`.
161+
Let's consider the simproc `reduceIte` (also in the first post!).
162+
This takes expressions of the form `if h then a else b` and outputs `a`
163+
(resp. `b`) if `h` can be simplified to `True` (resp. `False`).
164+
Since `a` and `b` could be arbitrarily complicated expressions, it makes sense to try and simplify them further,
165+
so this simproc uses `visit` to output the result of this simplification.
140166
- `continue`.
141167
It turns out that both `Nat.reduceDvd` and `reduceIte` also use the `continue` constructor.
142-
Indeed, both these simprocs rely on the fact that some part of the expression considered is simplifiable (e.g. the condition in the `if` statement).
143-
If that is not the case we need a way to signal to `simp` that it should *not* attempt to simplify the expression again using the same simproc to prevent the simplification procedure from endlessly looping around.
144-
This is precisely what the `continue` constructor allows us to do.
145-
More generally, `continue` is used in most simprocs as the "default" output produced when the simproc was not able to make any simplification.
146-
147-
In the case where the two expressions `e` and `e'` are definitionally equal, one can actually describe a simplification step using a simple structure, namely `DStep` (where the "d" stands for "definitional").
148-
This is obtained by replacing each occurrence of `Result` in the definition of `Step` by `Expr` (intuitively, we no longer need to specify a proof that `e` and `e'` are equal since this is just `rfl`, so we only need to return the simplified expression `e'`):
168+
Indeed, both these simprocs rely on the fact that some part of the expression
169+
considered is simplifiable (e.g. the condition in the `if` statement).
170+
When this is not the case, the `continue` constructor signals to `simp` that it should
171+
*not* attempt to simplify the expression again using the same simproc to prevent
172+
the simplification procedure from looping around.
173+
More generally, `continue` is used in most simprocs as the "default" output
174+
produced when the simproc was not able to make any simplification.
175+
176+
In the case where the two expressions `e` and `e'` are definitionally equal,
177+
one can actually describe a simplification step using a simple structure,
178+
namely `DStep` (where the "d" stands for "definitional").
179+
This is obtained by replacing each occurrence of `Result` in the
180+
definition of `Step` by `Expr` (intuitively, we no longer need to specify a
181+
proof that `e` and `e'` are equal since this is just `rfl`, so we only need to return the simplified expression `e'`):
149182
```lean
150183
inductive DStep where
151184
/-- Return expression without visiting any subexpressions. -/
@@ -188,7 +221,8 @@ Let's go through these steps one by one.
188221
- What local variables/declarations we have access to
189222

190223
2) The first monad transformer application: `StateRefT Simp.State MetaM`.
191-
The idea here is the following: since the goal of the `SimpM` monad is to track the state of a `simp` call (i.e. what's happening, as the program runs), we need to capture more information than what `MetaM` gives us.
224+
The idea here is the following: since the goal of the `SimpM` monad is to track the state of a `simp` call
225+
(i.e. what's happening, as the program runs), we need to capture more information than what `MetaM` gives us.
192226
Specifially, we want a monad that can track the state of what's happening via the following structure:
193227
```lean
194228
structure Simp.State where
@@ -202,12 +236,9 @@ Let's go through these steps one by one.
202236
This is something we can achieve using the `StateRefT` monad transformer, which takes as input a state type (`Simp.State` in our case) and a monad, and creates a new monad that can read _and write_ this state. In other words, `StateRefT Simp.State MetaM` is a souped up version of `MetaM` that can now track extra information by storing (and updating) at term of type `Simp.State`.
203237

204238
3) The second monad transformer application: `ReaderT Simp.Context $ StateRefT Simp.State MetaM`.
205-
Depending on where/how the `simp` tactic is called, the amount of "information" it has access to might vary.
206-
For example, the adding new imports will give `simp` access to more `simp` theorems, or the user may choose to provide additional theorems or fact to `simp` to make it more powerful (in effect, these are treated as extra `simp` theorems).
207-
This is also something we need to capture in the monad we're building: we now want to give the monad access to an extra "context" variable, which will be a term of type `Simp.Context`.
208-
The astute reader will have noticed that the situation is not quite the same as when we were adding a `Simp.State` state to `MetaM`: while we will often want to change the state during the `simp` call, the context should always be the same.
209-
In programmer lingo, one might say that the context should be _immutable_.
210-
Thus, we should use a different monad transformer called `ReaderT`, which takes as input a "context" type and a monad, and outputs a new monad that has reading access to the context, but _cannot change it_.
239+
The `SimpM` monad should also be able to access the "context" that `simp` is running in, e.g. which simp theorems it has access to and so on. This is captured by the type `Simp.Context`.
240+
Here, the situation is not quite the same as when we were adding a `Simp.State` state to `MetaM`: while we will often want to change the state during the `simp` call, the context should always be the same (in programmer lingo: _immutable_)
241+
Thus, we use a different monad transformer called `ReaderT`, which is almost identical to `StateT`, but outputs a new monad that can only read the type passed as parameter.
211242

212243
4) The final monad transformer application: `ReaderT Simp.MethodsRef $ ReaderT Simp.Context $ StateRefT Simp.State MetaM`.
213244
This outputs a monad that has access to `Simp.Method` (passed via a ref).

0 commit comments

Comments
 (0)