Skip to content

Conversation

@YaelDillies
Copy link
Collaborator

This is the second part of #97.

Co-authored-by: Paul Lezeau [email protected]

@YaelDillies YaelDillies force-pushed the how-to-write-a-simproc branch from 5ed721a to 2feca6c Compare April 16, 2025 10:30
@YaelDillies YaelDillies changed the title feat: How to write a simproc feat: Simp, made simple Apr 19, 2025
YaelDillies pushed a commit that referenced this pull request May 26, 2025
This PR adds the first blog post in a series of three to introduce the
concept of simprocs and explain how to write them.

The second blog post can be found here: #98

Co-authored-by : Yaël Dillies <[email protected]>
@bryangingechen
Copy link
Contributor

Part 1 references this post, so maybe it can also be updated to have working links pointing here too.

```
Note: The above snippet is a simplification and the constructors as shown actually belong to `Lean.TransformStep`, which `Lean.Meta.Simp.DStep` is an `abbrev` of.

<span style="color:red">**(Yaël): Why is there a mismatch in docstrings between `Step.continue` and `DStep.continue`? [Zulip](https://leanprover.zulipchat.com/#narrow/channel/270676-lean4/topic/Simp.2EStep.2Econtinue.20vs.20Simp.2EDStep.2Econtinue/with/509056271)**</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to remove this?

@YaelDillies YaelDillies force-pushed the how-to-write-a-simproc branch 2 times, most recently from ed3714e to 04f3d50 Compare June 3, 2025 17:52
@YaelDillies YaelDillies force-pushed the how-to-write-a-simproc branch from 04f3d50 to f7aa206 Compare June 8, 2025 14:06
Copy link

@edegeltje edegeltje left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx for writing this

Co-authored-by: blizzard_inc <[email protected]>
```

`simp` does not consume bare elements of type `Simproc`.
Instead, a simproc is an element of type `Simproc` annotated with tbe extra data mentioned in the overview subsection, like whether the simproc is `pre` or `post`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section on Step is great, and contains a lot of detail. These latter sections seem somewhat rushed and the ending of the blogpost feels abrupt to me.
For example, this sentence about annotations with extra data... could that be accompanied by a code block that shows exactly what is going on?

And would it make sense to give the exact definition of the simpM monad, and add a bit more detail on it?

Maybe you could even end with a little cliff hanger about what will happen in the final blogpost?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I'm planning on spending some extra time adding stuff about SimpM to the post this coming month.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏓 @Paul-Lez and @YaelDillies, it would be great to get this out! :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kim-em this is now ready to review if you fancy taking a look;)
If you're very busy, I think the first part is probably more ready/polished than the second one!

@Paul-Lez Paul-Lez force-pushed the how-to-write-a-simproc branch from 206fa34 to 3ac5a4c Compare September 10, 2025 18:17
Comment on lines 166 to 174
- `continue`.
It turns out that both `Nat.reduceDvd` and `reduceIte` also use the `continue` constructor.
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).
When this is not the case, the `continue` constructor signals to `simp` that it should
*not* attempt to simplify the expression again using the same simproc to prevent
the simplification procedure from looping around.
More generally, `continue` is used in most simprocs as the "default" output
produced when the simproc was not able to make any simplification.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't completely understand this explanation, it seems like there are two things being mixed up here. Step indicates what to do when the simproc is done, right? But here "some part of the expression considered is simplifiable (e.g. the condition in the if statement)." refers to simplifying before the logic of the simproc itself. And then you describe when not to use .continue instead of when you do want to use it. Also because the .visit/.continue distinction is only relevant for pre-simprocs, which is mentioned above, but not here.

If we want to rework this completely, I would do something like: open the list with .continue since .continue none is the default. Then say something like:

... .continue indicates that the simproc is done with this expression and its subexpressions. As a result, simp will not not attempt to simplify the expression again using the same simproc to prevent the simplification procedure from looping around.


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").
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DSimprocs are mentioned only later on. So it is weird to discuss DStep here, it will make the reader wonder how they can return a DStep instead of a Step from a simproc.

3) The second monad transformer application: `ReaderT Simp.Context $ StateRefT Simp.State MetaM`.
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`.
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_)
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mildly pedantic, but ReaderT can change the value in the calls it makes (e.g. ReaderT.adapt as equivalent to modify for a Reader). In programming language nerd terms, State(Ref)T gives you a global variable, and ReaderT gives you dynamic scoping.

(Does simp in practice use that though?)

Copy link
Contributor

@Vierkantor Vierkantor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few places where things are not clear to me, but you did mention everything I was expecting to see. Did not yet read it closely for style.

Copy link
Collaborator Author

@YaelDillies YaelDillies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first 140 lines flow really well! Let's make sure this is also the case for the other 200 lines after that.

2. Traverse subexpressions of `e` (note that the preprocedures might have changed `e` by this point);
3. Run postprocedures on `e`.

We call it the *simplification loop*.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
We call it the *simplification loop*.
We call this the *simplification loop*.

Both `visit` and `done` signify success.

Whenever a simproc is called on a given expression, it outputs a `Step`, which determines what will happen next during the `simp` call.
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.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
Since every simproc call is running a metaprogram to produce the output `Step`, the constructor that ends being up used may vary according to the input.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
Since every simproc call is running a metaprogram to produce the output `Step`, the constructor that ends up being used may vary according to the input.


Whenever a simproc is called on a given expression, it outputs a `Step`, which determines what will happen next during the `simp` call.
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.
For example, a given simproc may in some cases use `visit` and in others use `continue`.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence is quite strange to me: it bases itself off the assumption that people expect that a simproc always uses the same Step constructor, but four lines above we explain how to use the constructors according to whether the simproc has failed or not. Could you repackage the info in a way that acknowledges the previous sentence?

The constructors do the following:
- `continue` indicates that the simproc is done with this expression.
As a result, simp will not not attempt to simplify the expression again using the same simproc to prevent the simplification procedure from looping around.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
As a result, simp will not not attempt to simplify the expression again using the same simproc to prevent the simplification procedure from looping around.
As a result, simp will not attempt to simplify the expression again using the same simproc to prevent the simplification procedure from looping.

Comment on lines +170 to +173
Recall from the first post the simproc `Nat.reduceDvd`.
This takes expressions of the form `a | b` where `a`, `b` are explicit natural numbers, and returns `True` or `False`.
Either way, the output is in simp normal form, so there is no need to attempt to simplify it further.
Thus, the simproc uses `done` to this result.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already recalled on L.156

Suggested change
Recall from the first post the simproc `Nat.reduceDvd`.
This takes expressions of the form `a | b` where `a`, `b` are explicit natural numbers, and returns `True` or `False`.
Either way, the output is in simp normal form, so there is no need to attempt to simplify it further.
Thus, the simproc uses `done` to this result.
When `Nat.reduceDvd` is called on an expression of the form `a | b` where `a`, `b` are explicit natural numbers, it simplifies it to `True` or `False`.
Either way, the output is in simp normal form and there is no need to simplify it further.
Thus `Nat.reducedDvd` uses `done` in such a case.

## Exploring the `SimpM` monad via simprocs

In the next blog post, we will cover in detail how to implement simprocs that are useful for proving theorems in Lean.
In the meantime, to whet the reader's appetite, let's have a go at exploring the `SimpM` monad's internals using simprocs.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the meantime, to whet the reader's appetite, let's have a go at exploring the `SimpM` monad's internals using simprocs.
In the meantime, to whet the reader's appetite, let's explore the internals of the `SimpM` monad using fake simprocs that print info instead of simplifying.

In the next blog post, we will cover in detail how to implement simprocs that are useful for proving theorems in Lean.
In the meantime, to whet the reader's appetite, let's have a go at exploring the `SimpM` monad's internals using simprocs.

> Throughout this section, all code will be assumed to have `open Lean Elab Meta Simp`.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> Throughout this section, all code will be assumed to have `open Lean Elab Meta Simp`.
> Throughout this section, all code is assumed to be prefaced with `open Lean Elab Meta Simp`.

-- declare the simproc
simproc_decl printExpr (_) := printExpressions
```
The last line is needed to "declare" the simproc officially - this is where we specify information like whether this is a pre/post procedure, and what expression we're matching on (here, we match on the pattern `_`, i.e. on everything!).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The last line is needed to "declare" the simproc officially - this is where we specify information like whether this is a pre/post procedure, and what expression we're matching on (here, we match on the pattern `_`, i.e. on everything!).
The last line is needed to "declare" the simproc officially - this is where we can specify priority, whether this is a pre/post procedure and what expression we're matching on (here, we match on the pattern `_`, i.e. on everything!).

We didn't mention the pattern on L.46-47. Should we?

The last line is needed to "declare" the simproc officially - this is where we specify information like whether this is a pre/post procedure, and what expression we're matching on (here, we match on the pattern `_`, i.e. on everything!).
More on this in the next post.

Next, we could try an print out theorems that have used by `simp` "so far".
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Next, we could try an print out theorems that have used by `simp` "so far".
Next, let's print out all the theorems that have been used by `simp` "so far".

simproc_decl printThms (_) := printUsedTheorems
```

The reader can then call add these simprocs to simp calls to see what's happening within.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The reader can then call add these simprocs to simp calls to see what's happening within.
We encourage the reader to add these simprocs to their simp calls to see what's happening within.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.