You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: posts/simp-made-simple.md
+85-54Lines changed: 85 additions & 54 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,6 +9,8 @@ slug: simp-made-simple
9
9
tags: 'simp, simproc, meta'
10
10
title: Simp, made simple.
11
11
type: text
12
+
header-includes:
13
+
- \usepackage{tikz}
12
14
---
13
15
14
16
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
60
62
61
63
We call it the *simplification loop*.
62
64
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
+
63
89
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,
65
91
as we shall see in the coming subsection.
66
92
67
93
## `Step`
@@ -75,39 +101,35 @@ This is encapsulated by the [`Result`](https://leanprover-community.github.io/ma
75
101
structure:
76
102
```lean
77
103
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
80
108
-- Note, `Result` currently has an extra `cache` field that is both deprecated
81
109
-- and irrelevant to our current discussion.
82
110
```
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`:
97
130
```lean
98
131
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. -/
111
133
| visit (e : Result)
112
134
/--
113
135
For `pre` procedures, continue transformation by visiting subexpressions, and then
@@ -116,36 +138,47 @@ inductive Step where
116
138
For `post` procedures, this is equivalent to returning `visit`.
117
139
-/
118
140
| continue (e? : Option Result := none)
141
+
/-- Returns the result without visiting any subexpressions. -/
142
+
| done (r : Result)
119
143
```
144
+
> If a procedure fails to simplify an expression, it should return `continue none`.
145
+
Both `visit` and `done` signify success.
146
+
120
147
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.
127
148
128
149
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.
130
151
For example, a given simproc may in some cases use `visit` and in others use `continue`.
131
152
132
153
To make this more concrete, let's take a look at of each of these use cases in action.
133
154
134
-
-`done`. Recall from the first post the simproc `Nat.reduceDvd`.
155
+
-`done`.
156
+
Recall from the first post the simproc `Nat.reduceDvd`.
135
157
This takes expressions of the form `a | b` where `a`, `b` are explicit natural numbers, and returns `True` or `False`.
136
158
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.
140
166
-`continue`.
141
167
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'`):
149
182
```lean
150
183
inductive DStep where
151
184
/-- Return expression without visiting any subexpressions. -/
@@ -188,7 +221,8 @@ Let's go through these steps one by one.
188
221
- What local variables/declarations we have access to
189
222
190
223
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.
192
226
Specifially, we want a monad that can track the state of what's happening via the following structure:
193
227
```lean
194
228
structure Simp.State where
@@ -202,12 +236,9 @@ Let's go through these steps one by one.
202
236
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`.
203
237
204
238
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.
211
242
212
243
4) The final monad transformer application: `ReaderT Simp.MethodsRef $ ReaderT Simp.Context $ StateRefT Simp.State MetaM`.
213
244
This outputs a monad that has access to `Simp.Method` (passed via a ref).
0 commit comments