Compare commits

..

62 Commits

Author SHA1 Message Date
Kim Morrison
22ca91c0b9 chore: remove unneeded Batteries reference in repeat doc-string 2024-06-04 10:46:42 +10:00
Joachim Breitner
f65e3ae985 feat: simp to still work even if one simp arg does not work (#4177)
this fixes a usability paper cut that just annoyed me. When editing a
larger simp proof, I usually want to see the goal state after the simp,
and this is what I see while the `simp` command is complete. But then,
when I start typing, and necessarily type incomplete lemma names, that
error makes `simp` do nothing again and I see the original goal state.
In fact, if a prefix of the simp theorem name I am typing is a valid
identifier, it jumps even more around.

With this PR, using `logException`, I still get the red squiggly lines
for the unknown identifer, but `simp` just ignores that argument and
still shows me the final goal. Much nicer.

I also demoted the message for `[-foo]` when `foo` isn’t `simp` to a
warning and gave it the correct `ref`.

See it in action here: (in the middle, when you suddenly see the
terminal,
I am switching lean versions.)


https://github.com/leanprover/lean4/assets/148037/8cb3c563-1354-4c2d-bcee-26dfa1005ae0
2024-06-03 14:21:31 +00:00
Siddharth
81f5b07215 feat: getLsb_sshiftRight (#4179)
In the course of the development, I grabbed facts about right shifting
over integers [from
`mathlib4`](https://github.com/leanprover-community/mathlib4/blob/master/Mathlib/Data/Int/Bitwise.lean).

The core proof strategy is to perform a case analysis of the msb:
- If `msb = false`, then `sshiftRight = ushiftRight`.
- If `msb = true`. then `x >>>s i = ~~~(~~~(x >>>u i))`. The double
negation introduces the high `1` bits that one expects of the arithmetic
shift.

---------

Co-authored-by: Kim Morrison <scott@tqft.net>
2024-06-01 16:43:11 +00:00
Siddharth
9a597aeb2e feat: getLsb_{rotateLeft, rotateRight} (#4257)
These will be used by LeanSAT for bitblasting rotations by constant
distances.

We first reduce the case when the rotation amount is larger than the
width to the case where the rotation amount is less than the width
(`x.rotateLeft/Right r = x.rotateLeft/Right (r%w)`).

Then, we case analyze on the low bits versus the high bits of the
rotation, where we prove equality by extensionality.

---------

Co-authored-by: Alex Keizer <alex@keizer.dev>
Co-authored-by: Tobias Grosser <github@grosser.es>
2024-06-01 16:42:10 +00:00
Tobias Grosser
ff116dae5f feat: add BitVec _assoc lemmas (#4299) 2024-06-01 16:24:18 +00:00
John Tristan
0dff5701af doc: updated build instructions for mac os X (#4317)
Edit to the platform specific setup for mac os X. The installation of
llvm does not accept the options --with-clang and --with-asan anymore.
2024-06-01 16:23:17 +00:00
Kim Morrison
299cb9a806 chore: remove @[simp] from bind_eq_some (#4314) 2024-06-01 16:04:02 +00:00
Leonardo de Moura
b53a74d6fd fix: miscompilation in constant folding (#4311)
closes #4306
2024-05-31 04:24:45 +00:00
Lean stage0 autoupdater
007b423006 chore: update stage0 2024-05-30 09:57:02 +00:00
Sebastian Ullrich
6c63c9c716 feat: quotations for parser aliases (#4307)
Another papercut towards incremental `have`
2024-05-30 09:22:22 +00:00
Kim Morrison
8bbb015a97 chore: add namespace in Init/Data/Fin/Fold (#4304) 2024-05-29 16:40:55 +00:00
Alex Keizer
9133470243 feat: upstream BitVec.toFin_ofNat and BitVec.toFin_neg (#4298)
These lemmas are morally equivalent to Mathlib lemmas which are proposed
to be deleted from Mathlib in
[#13286](https://github.com/leanprover-community/mathlib4/pull/13286).

It is only morally equivalent, because the Mathlib lemmas are stated in
terms of Mathlib-defined things: `toFin_natCast` uses a coercion from
`Nat` to `Fin (2^w)` which relies on `NeZero` machinery available only
in Mathlib. Thus, I've rephrased the rhs in terms of the def-eq
`Fin.ofNat'` with an explicit proof that `2^w` is non-zero.

Similarly, the RHS of `toFin_neg` was phrased in terms of negation on
`Fin`s, which is only defined in Mathlib, so I've unfolded the
definition.
2024-05-29 08:25:51 +00:00
Markus Himmel
d07b316804 fix: incorrect docstring for named pattern syntax (#4294)
---
2024-05-29 08:23:15 +00:00
Wojciech Nawrocki
ec59e7a2c0 feat: widget messages (#4254)
Allows embedding user widgets in structured messages. Companion PR is
leanprover/vscode-lean4#449.

Some technical choices:
- The `MessageData.ofWidget` constructor might not be strictly necessary
as we already have `MessageData.ofFormatWithInfos`, and there is
`Info.ofUserWidget`. However, `.ofUserWidget` also requires a `Syntax`
object (as it is normally produced when widgets are saved at a piece of
syntax during elaboration) which we do not have in this case. More
generally, it continues to be a bit cursed that `Elab.Info` nodes are
used both for elaboration and delaboration (pretty-printing), so
entrenching that approach seems wrong. The better approach would be to
have a separate notion of pretty-printer annotation; but such a refactor
would not be clearly beneficial right now.
- To support non-JS-based environments such as
https://github.com/Julian/lean.nvim, `.ofWidget` requires also providing
another message which approximates the widget in a textual form.
However, in practice these environments might still want to support a
few specific user widgets such as "Try this".

---

Closes #2064.
2024-05-29 06:37:42 +00:00
Sebastian Ullrich
cc33c39cb0 chore: bootstrap fixes 2024-05-28 23:04:19 +02:00
Sebastian Ullrich
8c7364ee64 chore: update stage0 2024-05-28 23:04:19 +02:00
Sebastian Ullrich
26b6718422 chore: haveId node kind 2024-05-28 23:04:19 +02:00
Sebastian Ullrich
66777670e8 fix: stray tokens in tactic block should not inhibit incrementality (#4268) 2024-05-27 07:36:13 +00:00
Sebastian Ullrich
f05a82799a chore: CI: restore macOS aarch64 install sufix 2024-05-26 14:29:56 +02:00
Leonardo de Moura
8eee5ff27f fix: do not include internal match equational theorems at simp trace (#4274)
closes #4251
2024-05-25 17:16:19 +00:00
Mac Malone
fe17b82096 refactor: lake: ensure job actions can be lifted to FetchM (#4273)
In `v4.8.0-rc2`, due to additional build refactor changes, `JobM` no
longer cleanly lifts in `FetchM`. Generally, a `JobM` action should not
be run `FetchM` directly but spawned asynchronously as job (e.g., via
`Job.async`). However, there may be some edge cases were this is
necessary and it is a backwards compatibility break, so this change adds
back the lift. This change also includes an `example` definition to
ensure the lift works in order to prevent similar accidental breakages
in the future.

This breakage was first reported by Mario on
[Zulip](https://leanprover.zulipchat.com/#narrow/stream/113488-general/topic/v4.2E8.2E0-rc2.20discussion/near/440407037).
2024-05-25 02:20:04 +00:00
Mac Malone
def00d3920 refactor: lake: manifest semver & code cleanup (#4083)
Switches the manifest format to use `major.minor.patch` semantic
versions. Major version increments indicate breaking changes (e.g., new
required fields and semantic changes to existing fields). Minor version
increments (after `0.x`) indicate backwards-compatible extensions (e.g.,
adding optional fields, removing fields). This change is backwards
compatible. Lake will still successfully read old manifest with numeric
versions. It will treat the numeric version `N` as semantic version
`0.N.0`. Lake will also accept manifest versions with `-` suffixes
(e.g., `x.y.z-foo`) and then ignore the suffix.

This change also includes the general cleanup/refactoring of the
manifest code and data structures that was part of #3174.
2024-05-24 21:32:41 +00:00
Mac Malone
cd16975946 feat: lake pack / lake unpack (#4270)
Adds two new Lake commands, `lake pack` and `lake unpack`, which pack
and unpack, respectively, Lake build artifacts from an archive. If a
path argument is given, creates the archive specified, otherwise uses
the information in a package's `buildArchive` configuration as the
default.

The pack command will be used by Reservoir to prepare crate-style build
archives for packages. In the future, the command will also be
extensible through configuration file hooks.
2024-05-24 21:32:07 +00:00
Mac Malone
0448e3f4ea feat: lake test improvements & lake lint (#4261)
Extends the functionality of `lake test` and adds a parallel command in
`lake lint`.

* Rename `@[test_runner]` / `testRunner` to `@[test_driver]` /
`testDriver`. The old names are kept as deprecated aliases.
* Extend help page for `lake test` and adds one for `lake check-test`. 
* Add `lake lint` and its parallel tag `@[lint_driver]` , setting
`lintDriver`, and checker `lake check-lint`.
* Add support for specifying test / lint drivers from dependencies. 
* Add `testDriverArgs` / `lintDriverArgs` for fixing additional
arguments to the invocation of a driver script or executable.
* Add support for library test drivers (but not library lint drivers). 
* `lake check-test` / `lake check-lint` only load the package (without
dependencies), not the whole workspace.

Closes #4116. Closes #4121. Closes #4142.
2024-05-24 21:31:41 +00:00
Sebastian Ullrich
d3ee0be908 feat: show signature elaboration errors on body parse error (#4267)
Fixes #3556

---------

Co-authored-by: Joachim Breitner <mail@joachim-breitner.de>
2024-05-24 10:07:07 +00:00
Sebastian Ullrich
d1a96f6d8f chore: CI: native-compile aarch64 macOS (#4265) 2024-05-24 08:18:49 +00:00
Kim Morrison
b0c1112471 chore: better omega error message if no facts found (#4264) 2024-05-24 05:15:15 +00:00
JovanGerb
e5e5a4d2e0 chore: fix the MonadStore type classes, with semiOutParam (#4205)
The type class `MonadStore1` and friends have an outParam, which should
not be an outParam, because there are multiple possible values for this
parameter. At this function
[fetchOrCreate](1382e9fbc4/src/lake/Lake/Load/Main.lean (L196C49-L196C63)),
there are multiple stacked `StateT` monad transformers that each give a
different instance to `MonadStore1`. It is an implementation detail of
type class synthesis which instance is found. This particular type class
synthesis fails when the unused instance
`Lake.instMonadStore1OfMonadDStoreOfFamilyOut` is set to a lower
priority, because then the synthesis order happens to go differently, so
the wrong instance is found.

Replacing the outParam with a semiOutParam solves this issue. Thus, we
make a new type class `MonadStore1Of`, which is the same, but with a
semiOutParam. This follows the design of `MonadState` and
`MonadStateOf`.

However, then it turns out that the instance cannot anymore be
synthesised.

There are two instances for `MonadStore1`:
```
instance [MonadDStore κ β m] : MonadStore1 k (β k) m
instance [MonadDStore κ β m] [FamilyOut β k α] : MonadStore1 k α m 
```
The first one is problematic during unification, especially when `β`
should be instantiated as a constant function. We make the second one
sufficient by adding an instance for the general type family:
```
/-- The general type family -/
instance (priority := low) : FamilyDef Fam a (Fam a) where
  family_key_eq_type := rfl
```
So then we can get rid of the first instance.
2024-05-24 02:09:07 +00:00
Sebastian Ullrich
e020f3d159 chore: CI: move some expensive checks from merge queue to releases (#4255) 2024-05-23 20:45:44 +00:00
Sebastian Ullrich
811bad16e1 fix: ensure incremental commands and tactics are reached only on supported paths (#4259)
Without this, it would not easy but perhaps be feasible to break
incrementality when editing command prefixes such as `set_option ... in
theorem` or also `theorem namesp.name ...` (which is a macro),
especially if at some later point we support incrementality in input
shifted by an edit. Explicit, sound support for these common cases will
be brought back soon.
2024-05-23 17:57:42 +00:00
Sebastian Ullrich
67338bac23 chore: replace registerBuiltinIncrementalTactic with @[builtin_incremental] 2024-05-23 17:53:58 +02:00
Sebastian Ullrich
ba629545cc chore: update stage0 2024-05-23 17:26:21 +02:00
Sebastian Ullrich
dfb496a271 fix: allow multiple declareBuiltin per declaration 2024-05-23 17:23:39 +02:00
Sebastian Ullrich
250994166c feat: [(builtin_)incremental] elab attribute 2024-05-23 17:23:39 +02:00
Sebastian Ullrich
73a0c73c7c chore: modernize build instructions (#4032)
Use `cmake --preset`, adjust and document parallelism settings
2024-05-23 10:55:07 +00:00
meow-sister
258cc28dfc fix: docstring in Attributes.lean (#4238)
Changing document string in `Attributes.lean`, in order to consistent
with code in `Lean.Parser.Attr`.

---------

Co-authored-by: Kim Morrison <kim@tqft.net>
2024-05-23 10:13:43 +00:00
Sebastian Ullrich
f61a64d2ff chore: reset LEAN_VERSION_IS_RELEASE 2024-05-23 12:28:24 +02:00
Kyle Miller
d984030c6a chore: set up procedure for preparing release notes (#4247)
We are switching to a new system for preparing release notes.
* Release notes will be compiled when creating a release candidate from
all the commits that are part of that release.
* PRs can include suggestions for release notes in PR messages. Please
use language such as "release notes" and "breaking changes" to call
attention to the suggestions. Release notes are user-centric rather than
developer-centric.
* For more complicated release notes, these can be put into the
`releases_drafts` folder.

This solves an issue where PRs that include release notes can, when
merged, have those notes appear under the wrong Lean version, since they
might have been created before a release but not merged until after. It
also solves merge conflicts due to multiple PRs updating the release
notes.
2024-05-23 02:46:11 +00:00
FR
93758cc222 perf: faster Nat.testBit (#4188)
`1 &&& n` is faster than `n &&& 1` for big `n`.

---
2024-05-23 01:34:40 +00:00
Alex Keizer
4fa3b3c4a0 feat: bitblasting theorems for signed comparisons (#4201)
Prove theorems that relate `BitVec.slt` and `sle` to `carry`, so that
these signed comparisons may be bitblasted in LeanSAT.

This PR is stacked on top of #4200. For the diff without changes from
that PR, see:
https://github.com/opencompl/lean4/compare/opencompl:lean4:bitvec-toInt-iff-msb...bitvec-slt-blast

---------

Co-authored-by: Kim Morrison <scott@tqft.net>
2024-05-23 01:24:04 +00:00
Leonardo de Moura
2bc41d8f3a fix: case tactic in macros (#4252)
We must erase macro scopes for tags in `case` as we do in `cases .. with
..` and `induction .. with ..`.
2024-05-23 00:01:24 +00:00
Sebastian Ullrich
f97a7d4234 feat: incremental elaboration of definition headers, bodies, and tactics (#3940)
Extends Lean's incremental reporting and reuse between commands into
various steps inside declarations:
* headers and bodies of each (mutual) definition/theorem
* `theorem ... := by` for each contained tactic step, including
recursively inside supported combinators currently consisting of
  * `·` (cdot), `case`, `next`
  * `induction`, `cases`
  * macros such as `next` unfolding to the above

![Recording 2024-05-10 at 11 07
32](https://github.com/leanprover/lean4/assets/109126/c9d67b6f-c131-4bc3-a0de-7d63eaf1bfc9)

*Incremental reuse* means not recomputing any such steps if they are not
affected by a document change. *Incremental reporting* includes the
parts seen in the recording above: the progress bar and messages. Other
language server features such as hover etc. are *not yet* supported
incrementally, i.e. they are shown only when the declaration has been
fully processed as before.

---------

Co-authored-by: Scott Morrison <scott.morrison@gmail.com>
2024-05-22 13:23:30 +00:00
Alex Keizer
23a202b6be feat: characterize BitVec.toInt in terms of BitVec.msb (#4200)
This PR extracts `msb_eq_false_iff_two_mul_lt` and
`msb_eq_true_iff_two_mul_ge` from #4179, and uses them to prove a
theorem that characterizes `BitVec.toInt` in terms of `BitVec.msb`. This
lemma will be useful to prove a bit-blasting theorem for `BitVec.slt`
and `BitVec.sle`.

Also cleans up an existing proof (`toInt_eq_toNat_cond `), which turns
out to be provable by `rfl`.

---------

Co-authored-by: Kim Morrison <scott@tqft.net>
2024-05-22 11:14:37 +00:00
Leonardo de Moura
ff37e5d512 feat: add grind core module (#4249) 2024-05-22 03:50:36 +00:00
Kim Morrison
c2b8a1e618 chore: fix implicitness in HashMap.ofList (#4248)
Reported at
https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/Typeclass.20Inference.20Failure.20on.20HashMap/near/439842209
2024-05-22 03:04:37 +00:00
Mac Malone
8ca00918fb chore: lake: fix noRelease test when lean repo is tagged (#4244)
Fixes a bug with the Lake `noRelease` test where the test would fail if
the current revision of the Lean repository itself had a tag.
2024-05-21 22:51:33 +00:00
Leonardo de Moura
6155513c60 chore: update stage0
Motivation: `[grind_cases]` at `Init`
2024-05-21 21:46:23 +02:00
Leonardo de Moura
d6709eb157 feat: add [grind_cases] attribute 2024-05-21 21:46:23 +02:00
Leonardo de Moura
e6be8b90f5 feat: add grind.injection (#4243) 2024-05-21 17:57:02 +00:00
Joachim Breitner
82401938cf fix: hovers on binders with metavariables (#4192)
this fixes #4078. It is an alternative fix to the one in #4137,
suggested
by @kmill.

Incidentially, it makes the unused variable linter better. My theory is
that
if we don’t reset the info when backtracking, the binder shows up more
than
once in the info tree, and then it is considered “used”, although there
are
just multiple binders.
2024-05-21 10:34:58 +00:00
Kim Morrison
3de60bb1f6 chore: Lake details & improve formatting in changelog (#4239)
This is #4207, but targeting `master`.

Co-authored-by: Mac Malone <tydeu@hatpress.net>
2024-05-21 06:11:32 +00:00
Leonardo de Moura
8c03650359 feat: some Char, UInt, and Fin theorems (#4231)
for SSFT24 summer school: https://github.com/david-christiansen/ssft24

---------

Co-authored-by: Kim Morrison <kim@tqft.net>
Co-authored-by: Kim Morrison <scott.morrison@gmail.com>
Co-authored-by: David Thrane Christiansen <david@davidchristiansen.dk>
2024-05-21 06:11:23 +00:00
Austin Letson
2faa81d41f doc: add docstrings and examples for String functions (#4166)
Add docstrings, usage examples, and doc tests for `String.prev`,
`.front`, `.back`, `.atEnd`.

Improve docstring examples for `String.next` based on discussion
examples for `String.prev`.

---------

Co-authored-by: Kim Morrison <kim@tqft.net>
2024-05-21 04:27:40 +00:00
Siddharth
097a4d5b6b feat: define rotateLeft/Right with modulo (#4229)
This ensures that rotateLeft/Right behave correctly even when the
rotation amount is larger than the bitwidth.

This shall be followed up with `getLsb` theorems for rotations for
LeanSAT.

We choose to write `aux` definitions since it is cleaner to reason about
the `aux` theorems with the assumption that `rotation-amount <
bit-width`, followed by auxiliary lemmas that link the behavior of
rotation to the canonical case when `rotation-amount < bit-width`.

Proof strategy we will execute based on these definitions: [Link to
proof of
`getLsb_rotateLeft`](a0b18ec0f4/src/Init/Data/BitVec/Lemmas.lean (L1129-L1204))

---------

Co-authored-by: Tobias Grosser <github@grosser.es>
2024-05-21 03:49:09 +00:00
Mac Malone
f512826b9a chore: lake: spinner icon for computing step (#4236) 2024-05-21 03:48:39 +00:00
Leonardo de Moura
7c053259d3 feat: add grind.cases tactic (#4235) 2024-05-21 02:03:33 +00:00
Leonardo de Moura
f3ccd6b023 feat: some string simprocs (#4233)
For the SSFT24 summer school.
2024-05-20 22:53:10 +00:00
Mac Malone
7ece5d56e3 feat: lake: ANSI monitor: only display progress + running job info (#4232)
The ANSI mode build monitor now now longer displays built jobs (instead
only those that print info or failed). Also upgrades the progress ticker
with a spinner icon and information on the number of running jobs.
2024-05-20 20:29:21 +00:00
Kyle Miller
a7338c5ad8 feat: make frontend normalize line endings to LF (#3903)
To eliminate parsing differences between Windows and other platforms,
the frontend now normalizes all CRLF line endings to LF, like [in
Rust](https://github.com/rust-lang/rust/issues/62865).

Effects:
- This makes Lake hashes be faithful to what Lean sees (Lake already
normalizes line endings before computing hashes).
- Docstrings now have normalized line endings. In particular, this fixes
`#guard_msgs` failing multiline tests for Windows users using CRLF.
- Now strings don't have different lengths depending on the platform.
Before this PR, the following theorem is true for LF and false for CRLF
files.
```lean
example : "
".length = 1 := rfl
```

Note: the normalization will take `\r\r\n` and turn it into `\r\n`. In
the elaborator, we reject loose `\r`'s that appear in whitespace. Rust
instead takes the approach of making the normalization routine fail.
They do this so that there's no downstream confusion about any `\r\n`
that appears.

Implementation note: the LSP maintains its own copy of a source file
that it updates when edit operations are applied. We are assuming that
edit operations never split or join CRLFs. If this assumption is not
correct, then the LSP copy of a source file can become slightly out of
sync. If this is an issue, there is some discussion
[here](https://github.com/leanprover/lean4/pull/3903#discussion_r1592930085).
2024-05-20 17:13:08 +00:00
Leonardo de Moura
b278f9dd30 fix: missing withIncRecDepth and unifyEqs? and add support for offsets at unifyEq? (#4224)
Given `h` with type `x + k = y + k'` (or `h : k = k')`, `cases h`
produced a proof of size linear in `min k k'`. `isDefEq` has support for
offset, but `unifyEq?` did not have it, and a stack overflow occurred
while processing the resulting proof. This PR fixes this issue.

closes #4219
2024-05-20 13:42:36 +00:00
Eric Wieser
f0471a519b doc: upstream the Lean4 pygments lexer (#3125)
An improved `lean4` lexer is now part of pygments.

This depends on https://github.com/pygments/pygments/pull/2618 (now
merged), and [a subsequent
release](https://github.com/pygments/pygments/milestone/23)
2024-05-20 11:40:24 +00:00
Alex Keizer
42215cc072 feat: Nat.shiftLeft_shiftRight (#4199)
Show that shifting a natural number left and then shifting right by the
same amount is a no-op.

I originally proved this in a different PR, ended up not needing the
fact after all, but it still seemed like a generally useful simp lemma
to have.
2024-05-20 06:50:28 +00:00
591 changed files with 5559 additions and 2251 deletions

View File

@@ -20,8 +20,10 @@ jobs:
configure:
runs-on: ubuntu-latest
outputs:
# Should we run only a quick CI? Yes on a pull request without the full-ci label
quick: ${{ steps.set-quick.outputs.quick }}
# 0: PRs without special label
# 1: PRs with `merge-ci` label, merge queue checks, master commits
# 2: PRs with `release-ci` label, releases (incl. nightlies)
check-level: ${{ steps.set-level.outputs.check-level }}
# The build matrix, dynamically generated here
matrix: ${{ steps.set-matrix.outputs.result }}
# Should we make a nightly release? If so, this output contains the lean version string, else it is empty
@@ -38,167 +40,6 @@ jobs:
RELEASE_TAG: ${{ steps.set-release.outputs.RELEASE_TAG }}
steps:
- name: Run quick CI?
id: set-quick
# We do not use github.event.pull_request.labels.*.name here because
# re-running a run does not update that list, and we do want to be able to
# rerun the workflow run after settings the `full-ci` label.
run: |
if [ "${{ github.event_name }}" == 'pull_request' ]
then
echo "quick=$(gh api repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/pulls/${{ github.event.pull_request.number }} --jq '.labels | any(.name == "full-ci") | not')" >> "$GITHUB_OUTPUT"
else
echo "quick=false" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Configure build matrix
id: set-matrix
uses: actions/github-script@v7
with:
script: |
const quick = ${{ steps.set-quick.outputs.quick }};
console.log(`quick: ${quick}`);
// use large runners outside PRs where available (original repo)
// disabled for now as this mostly just speeds up the test suite which is not a bottleneck
// let large = ${{ github.event_name != 'pull_request' && github.repository == 'leanprover/lean4' }} ? "-large" : "";
let matrix = [
{
// portable release build: use channel with older glibc (2.27)
"name": "Linux LLVM",
"os": "ubuntu-latest",
"release": false,
"quick": false,
"shell": "nix develop .#oldGlibc -c bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-linux-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-linux.sh lean-llvm*",
"binary-check": "ldd -v",
// foreign code may be linked against more recent glibc
// reverse-ffi needs to be updated to link to LLVM libraries
"CTEST_OPTIONS": "-E 'foreign|leanlaketest_reverse-ffi'",
"CMAKE_OPTIONS": "-DLLVM=ON -DLLVM_CONFIG=${GITHUB_WORKSPACE}/build/llvm-host/bin/llvm-config"
},
{
"name": "Linux release",
"os": "ubuntu-latest",
"release": true,
"quick": true,
"shell": "nix develop .#oldGlibc -c bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-linux-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-linux.sh lean-llvm*",
"binary-check": "ldd -v",
// foreign code may be linked against more recent glibc
"CTEST_OPTIONS": "-E 'foreign'"
},
{
"name": "Linux",
"os": "ubuntu-latest",
"check-stage3": true,
"test-speedcenter": true,
"quick": false,
},
{
"name": "Linux Debug",
"os": "ubuntu-latest",
"quick": false,
"CMAKE_OPTIONS": "-DCMAKE_BUILD_TYPE=Debug",
// exclude seriously slow tests
"CTEST_OPTIONS": "-E 'interactivetest|leanpkgtest|laketest|benchtest'"
},
// TODO: suddenly started failing in CI
/*{
"name": "Linux fsanitize",
"os": "ubuntu-latest",
"quick": false,
// turn off custom allocator & symbolic functions to make LSAN do its magic
"CMAKE_OPTIONS": "-DLEAN_EXTRA_CXX_FLAGS=-fsanitize=address,undefined -DLEANC_EXTRA_FLAGS='-fsanitize=address,undefined -fsanitize-link-c++-runtime' -DSMALL_ALLOCATOR=OFF -DBSYMBOLIC=OFF",
// exclude seriously slow/problematic tests (laketests crash)
"CTEST_OPTIONS": "-E 'interactivetest|leanpkgtest|laketest|benchtest'"
},*/
{
"name": "macOS",
"os": "macos-13",
"release": true,
"quick": false,
"shell": "bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-apple-darwin.tar.zst",
"prepare-llvm": "../script/prepare-llvm-macos.sh lean-llvm*",
"binary-check": "otool -L",
"tar": "gtar" // https://github.com/actions/runner-images/issues/2619
},
{
"name": "macOS aarch64",
"os": "macos-13",
"release": true,
"quick": false,
"cross": true,
"cross_target": "aarch64-apple-darwin",
"shell": "bash -euxo pipefail {0}",
"CMAKE_OPTIONS": "-DUSE_GMP=OFF -DLEAN_INSTALL_SUFFIX=-darwin_aarch64",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-aarch64-apple-darwin.tar.zst https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-apple-darwin.tar.zst",
"prepare-llvm": "../script/prepare-llvm-macos.sh lean-llvm-aarch64-* lean-llvm-x86_64-*",
"binary-check": "otool -L",
"tar": "gtar" // https://github.com/actions/runner-images/issues/2619
},
{
"name": "Windows",
"os": "windows-2022",
"release": true,
"quick": false,
"shell": "msys2 {0}",
"CMAKE_OPTIONS": "-G \"Unix Makefiles\" -DUSE_GMP=OFF",
// for reasons unknown, interactivetests are flaky on Windows
"CTEST_OPTIONS": "--repeat until-pass:2",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-w64-windows-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-mingw.sh lean-llvm*",
"binary-check": "ldd"
},
{
"name": "Linux aarch64",
"os": "ubuntu-latest",
"CMAKE_OPTIONS": "-DUSE_GMP=OFF -DLEAN_INSTALL_SUFFIX=-linux_aarch64",
"release": true,
"quick": false,
"cross": true,
"cross_target": "aarch64-unknown-linux-gnu",
"shell": "nix develop .#oldGlibcAArch -c bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-linux-gnu.tar.zst https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-aarch64-linux-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-linux.sh lean-llvm-aarch64-* lean-llvm-x86_64-*"
},
{
"name": "Linux 32bit",
"os": "ubuntu-latest",
// Use 32bit on stage0 and stage1 to keep oleans compatible
"CMAKE_OPTIONS": "-DSTAGE0_USE_GMP=OFF -DSTAGE0_LEAN_EXTRA_CXX_FLAGS='-m32' -DSTAGE0_LEANC_OPTS='-m32' -DSTAGE0_MMAP=OFF -DUSE_GMP=OFF -DLEAN_EXTRA_CXX_FLAGS='-m32' -DLEANC_OPTS='-m32' -DMMAP=OFF -DLEAN_INSTALL_SUFFIX=-linux_x86",
"cmultilib": true,
"release": true,
"quick": false,
"cross": true,
"shell": "bash -euxo pipefail {0}"
},
{
"name": "Web Assembly",
"os": "ubuntu-latest",
// Build a native 32bit binary in stage0 and use it to compile the oleans and the wasm build
"CMAKE_OPTIONS": "-DCMAKE_C_COMPILER_WORKS=1 -DSTAGE0_USE_GMP=OFF -DSTAGE0_LEAN_EXTRA_CXX_FLAGS='-m32' -DSTAGE0_LEANC_OPTS='-m32' -DSTAGE0_CMAKE_CXX_COMPILER=clang++ -DSTAGE0_CMAKE_C_COMPILER=clang -DSTAGE0_CMAKE_EXECUTABLE_SUFFIX=\"\" -DUSE_GMP=OFF -DMMAP=OFF -DSTAGE0_MMAP=OFF -DCMAKE_AR=../emsdk/emsdk-main/upstream/emscripten/emar -DCMAKE_TOOLCHAIN_FILE=../emsdk/emsdk-main/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DLEAN_INSTALL_SUFFIX=-linux_wasm32",
"wasm": true,
"cmultilib": true,
"release": true,
"quick": false,
"cross": true,
"shell": "bash -euxo pipefail {0}",
// Just a few selected tests because wasm is slow
"CTEST_OPTIONS": "-R \"leantest_1007\\.lean|leantest_Format\\.lean|leanruntest\\_1037.lean|leanruntest_ac_rfl\\.lean\""
}
];
console.log(`matrix:\n${JSON.stringify(matrix, null, 2)}`)
if (quick) {
return matrix.filter((job) => job.quick)
} else {
return matrix
}
- name: Checkout
uses: actions/checkout@v3
# don't schedule nightlies on forks
@@ -249,6 +90,171 @@ jobs:
echo "Tag ${TAG_NAME} did not match SemVer regex."
fi
- name: Set check level
id: set-level
# We do not use github.event.pull_request.labels.*.name here because
# re-running a run does not update that list, and we do want to be able to
# rerun the workflow run after setting the `release-ci`/`merge-ci` labels.
run: |
check_level=0
if [[ -n "${{ steps.set-nightly.outputs.nightly }}" || -n "${{ steps.set-release.outputs.RELEASE_TAG }}" ]]; then
check_level=2
elif [[ "${{ github.event_name }}" != "pull_request" ]]; then
check_level=1
else
labels="$(gh api repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/pulls/${{ github.event.pull_request.number }}) --jq '.labels'"
if echo "$labels" | grep -q "release-ci"; then
check_level=2
elif echo "$labels" | grep -q "merge-ci"; then
check_level=1
fi
fi
echo "check-level=$check_level" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
- name: Configure build matrix
id: set-matrix
uses: actions/github-script@v7
with:
script: |
const level = ${{ steps.set-level.outputs.check-level }};
console.log(`level: ${level}`);
// use large runners outside PRs where available (original repo)
// disabled for now as this mostly just speeds up the test suite which is not a bottleneck
// let large = ${{ github.event_name != 'pull_request' && github.repository == 'leanprover/lean4' }} ? "-large" : "";
let matrix = [
{
// portable release build: use channel with older glibc (2.27)
"name": "Linux LLVM",
"os": "ubuntu-latest",
"release": false,
"check-level": 2,
"shell": "nix develop .#oldGlibc -c bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-linux-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-linux.sh lean-llvm*",
"binary-check": "ldd -v",
// foreign code may be linked against more recent glibc
// reverse-ffi needs to be updated to link to LLVM libraries
"CTEST_OPTIONS": "-E 'foreign|leanlaketest_reverse-ffi'",
"CMAKE_OPTIONS": "-DLLVM=ON -DLLVM_CONFIG=${GITHUB_WORKSPACE}/build/llvm-host/bin/llvm-config"
},
{
"name": "Linux release",
"os": "ubuntu-latest",
"release": true,
"check-level": 0,
"shell": "nix develop .#oldGlibc -c bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-linux-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-linux.sh lean-llvm*",
"binary-check": "ldd -v",
// foreign code may be linked against more recent glibc
"CTEST_OPTIONS": "-E 'foreign'"
},
{
"name": "Linux",
"os": "ubuntu-latest",
"check-stage3": level >= 2,
"test-speedcenter": level >= 2,
"check-level": 1,
},
{
"name": "Linux Debug",
"os": "ubuntu-latest",
"check-level": 2,
"CMAKE_PRESET": "debug",
// exclude seriously slow tests
"CTEST_OPTIONS": "-E 'interactivetest|leanpkgtest|laketest|benchtest'"
},
// TODO: suddenly started failing in CI
/*{
"name": "Linux fsanitize",
"os": "ubuntu-latest",
"check-level": 2,
// turn off custom allocator & symbolic functions to make LSAN do its magic
"CMAKE_PRESET": "sanitize",
// exclude seriously slow/problematic tests (laketests crash)
"CTEST_OPTIONS": "-E 'interactivetest|leanpkgtest|laketest|benchtest'"
},*/
{
"name": "macOS",
"os": "macos-13",
"release": true,
"check-level": 2,
"shell": "bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-apple-darwin.tar.zst",
"prepare-llvm": "../script/prepare-llvm-macos.sh lean-llvm*",
"binary-check": "otool -L",
"tar": "gtar" // https://github.com/actions/runner-images/issues/2619
},
{
"name": "macOS aarch64",
"os": "macos-14",
"CMAKE_OPTIONS": "-DLEAN_INSTALL_SUFFIX=-darwin_aarch64",
"release": true,
"check-level": 1,
"shell": "bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-aarch64-apple-darwin.tar.zst",
"prepare-llvm": "../script/prepare-llvm-macos.sh lean-llvm*",
"binary-check": "otool -L",
"tar": "gtar" // https://github.com/actions/runner-images/issues/2619
},
{
"name": "Windows",
"os": "windows-2022",
"release": true,
"check-level": 2,
"shell": "msys2 {0}",
"CMAKE_OPTIONS": "-G \"Unix Makefiles\" -DUSE_GMP=OFF",
// for reasons unknown, interactivetests are flaky on Windows
"CTEST_OPTIONS": "--repeat until-pass:2",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-w64-windows-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-mingw.sh lean-llvm*",
"binary-check": "ldd"
},
{
"name": "Linux aarch64",
"os": "ubuntu-latest",
"CMAKE_OPTIONS": "-DUSE_GMP=OFF -DLEAN_INSTALL_SUFFIX=-linux_aarch64",
"release": true,
"check-level": 2,
"cross": true,
"cross_target": "aarch64-unknown-linux-gnu",
"shell": "nix develop .#oldGlibcAArch -c bash -euxo pipefail {0}",
"llvm-url": "https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-x86_64-linux-gnu.tar.zst https://github.com/leanprover/lean-llvm/releases/download/15.0.1/lean-llvm-aarch64-linux-gnu.tar.zst",
"prepare-llvm": "../script/prepare-llvm-linux.sh lean-llvm-aarch64-* lean-llvm-x86_64-*"
},
{
"name": "Linux 32bit",
"os": "ubuntu-latest",
// Use 32bit on stage0 and stage1 to keep oleans compatible
"CMAKE_OPTIONS": "-DSTAGE0_USE_GMP=OFF -DSTAGE0_LEAN_EXTRA_CXX_FLAGS='-m32' -DSTAGE0_LEANC_OPTS='-m32' -DSTAGE0_MMAP=OFF -DUSE_GMP=OFF -DLEAN_EXTRA_CXX_FLAGS='-m32' -DLEANC_OPTS='-m32' -DMMAP=OFF -DLEAN_INSTALL_SUFFIX=-linux_x86",
"cmultilib": true,
"release": true,
"check-level": 2,
"cross": true,
"shell": "bash -euxo pipefail {0}"
},
{
"name": "Web Assembly",
"os": "ubuntu-latest",
// Build a native 32bit binary in stage0 and use it to compile the oleans and the wasm build
"CMAKE_OPTIONS": "-DCMAKE_C_COMPILER_WORKS=1 -DSTAGE0_USE_GMP=OFF -DSTAGE0_LEAN_EXTRA_CXX_FLAGS='-m32' -DSTAGE0_LEANC_OPTS='-m32' -DSTAGE0_CMAKE_CXX_COMPILER=clang++ -DSTAGE0_CMAKE_C_COMPILER=clang -DSTAGE0_CMAKE_EXECUTABLE_SUFFIX=\"\" -DUSE_GMP=OFF -DMMAP=OFF -DSTAGE0_MMAP=OFF -DCMAKE_AR=../emsdk/emsdk-main/upstream/emscripten/emar -DCMAKE_TOOLCHAIN_FILE=../emsdk/emsdk-main/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DLEAN_INSTALL_SUFFIX=-linux_wasm32",
"wasm": true,
"cmultilib": true,
"release": true,
"check-level": 2,
"cross": true,
"shell": "bash -euxo pipefail {0}",
// Just a few selected tests because wasm is slow
"CTEST_OPTIONS": "-R \"leantest_1007\\.lean|leantest_Format\\.lean|leanruntest\\_1037.lean|leanruntest_ac_rfl\\.lean\""
}
];
console.log(`matrix:\n${JSON.stringify(matrix, null, 2)}`)
return matrix.filter((job) => level >= job["check-level"])
build:
needs: [configure]
if: github.event_name != 'schedule' || github.repository == 'leanprover/lean4'
@@ -327,6 +333,9 @@ jobs:
# store in current directory, for easy uploading together with binary
echo $PWD/coredumps/%e.%p.%t | sudo tee /proc/sys/kernel/core_pattern
if: runner.os == 'Linux'
- name: Set up NPROC
run: |
echo "NPROC=$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 4)" >> $GITHUB_ENV
- name: Build
run: |
mkdir build
@@ -357,8 +366,8 @@ jobs:
OPTIONS+=(-DLEAN_SPECIAL_VERSION_DESC=${{ needs.configure.outputs.LEAN_SPECIAL_VERSION_DESC }})
fi
# contortion to support empty OPTIONS with old macOS bash
cmake .. ${{ matrix.CMAKE_OPTIONS }} ${OPTIONS[@]+"${OPTIONS[@]}"} -DLEAN_INSTALL_PREFIX=$PWD/..
make -j4
cmake .. --preset ${{ matrix.CMAKE_PRESET || 'release' }} -B . ${{ matrix.CMAKE_OPTIONS }} ${OPTIONS[@]+"${OPTIONS[@]}"} -DLEAN_INSTALL_PREFIX=$PWD/..
make -j$NPROC
make install
- name: Check Binaries
run: ${{ matrix.binary-check }} lean-*/bin/* || true
@@ -387,32 +396,29 @@ jobs:
build/stage1/bin/lean --stats src/Lean.lean
if: ${{ !matrix.cross }}
- name: Test
id: test
run: |
cd build/stage1
ulimit -c unlimited # coredumps
# exclude nonreproducible test
ctest -j4 --progress --output-junit test-results.xml --output-on-failure ${{ matrix.CTEST_OPTIONS }} < /dev/null
if: (matrix.wasm || !matrix.cross) && needs.configure.outputs.quick == 'false'
ctest --preset ${{ matrix.CMAKE_PRESET || 'release' }} --test-dir build/stage1 -j$NPROC --output-junit test-results.xml ${{ matrix.CTEST_OPTIONS }}
if: (matrix.wasm || !matrix.cross) && needs.configure.outputs.check-level >= 1
- name: Test Summary
uses: test-summary/action@v2
with:
paths: build/stage1/test-results.xml
# prefix `if` above with `always` so it's run even if tests failed
if: always() && (matrix.wasm || !matrix.cross) && needs.configure.outputs.quick == 'false'
if: always() && steps.test.conclusion != 'skipped'
- name: Check Test Binary
run: ${{ matrix.binary-check }} tests/compiler/534.lean.out
if: ${{ !matrix.cross && needs.configure.outputs.quick == 'false' }}
if: (!matrix.cross) && steps.test.conclusion != 'skipped'
- name: Build Stage 2
run: |
cd build
ulimit -c unlimited # coredumps
make -j4 stage2
make -C build -j$NPROC stage2
if: matrix.test-speedcenter
- name: Check Stage 3
run: |
cd build
ulimit -c unlimited # coredumps
make -j4 check-stage3
make -C build -j$NPROC stage3
if: matrix.test-speedcenter
- name: Test Speedcenter Benchmarks
run: |
@@ -423,11 +429,10 @@ jobs:
if: matrix.test-speedcenter
- name: Check rebootstrap
run: |
cd build
ulimit -c unlimited # coredumps
# clean rebuild in case of Makefile changes
make update-stage0 && rm -rf ./stage* && make -j4
if: matrix.name == 'Linux' && needs.configure.outputs.quick == 'false'
make -C build update-stage0 && rm -rf build/stage* && make -C build -j$NPROC
if: matrix.name == 'Linux' && needs.configure.outputs.check-level >= 1
- name: CCache stats
run: ccache -s
- name: Show stacktrace for coredumps

View File

@@ -7,7 +7,7 @@ on:
jobs:
restart-on-label:
runs-on: ubuntu-latest
if: contains(github.event.label.name, 'full-ci')
if: contains(github.event.label.name, 'merge-ci') || contains(github.event.label.name, 'release-ci')
steps:
- run: |
# Finding latest CI workflow run on current pull request

83
CMakePresets.json Normal file
View File

@@ -0,0 +1,83 @@
{
"version": 2,
"cmakeMinimumRequired": {
"major": 3,
"minor": 10,
"patch": 0
},
"configurePresets": [
{
"name": "release",
"displayName": "Default development optimized build config",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build/release"
},
{
"name": "debug",
"displayName": "Debug build config",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
},
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build/debug"
},
{
"name": "sanitize",
"displayName": "Sanitize build config",
"cacheVariables": {
"LEAN_EXTRA_CXX_FLAGS": "-fsanitize=address,undefined",
"LEANC_EXTRA_FLAGS": "-fsanitize=address,undefined -fsanitize-link-c++-runtime",
"SMALL_ALLOCATOR": "OFF",
"BSYMBOLIC": "OFF"
},
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build/sanitize"
},
{
"name": "sandebug",
"inherits": ["debug", "sanitize"],
"displayName": "Sanitize+debug build config",
"binaryDir": "${sourceDir}/build/sandebug"
}
],
"buildPresets": [
{
"name": "release",
"configurePreset": "release"
},
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "sanitize",
"configurePreset": "sanitize"
},
{
"name": "sandebug",
"configurePreset": "sandebug"
}
],
"testPresets": [
{
"name": "release",
"configurePreset": "release",
"output": {"outputOnFailure": true, "shortProgress": true}
},
{
"name": "debug",
"configurePreset": "debug",
"inherits": "release"
},
{
"name": "sanitize",
"configurePreset": "sanitize",
"inherits": "release"
},
{
"name": "sandebug",
"configurePreset": "sandebug",
"inherits": "release"
}
]
}

View File

@@ -1,171 +1,23 @@
# Lean 4 releases
This file contains release notes for each stable release.
Please check the [releases](https://github.com/leanprover/lean4/releases) page for the current status
of each version.
During development, drafts of future release notes appear in [`releases_drafts`](https://github.com/leanprover/lean4/tree/master/script).
We intend to provide regular "minor version" releases of the Lean language at approximately monthly intervals.
There is not yet a strong guarantee of backwards compatibility between versions,
only an expectation that breaking changes will be documented in this file.
This file contains work-in-progress notes for the upcoming release, as well as previous stable releases.
Please check the [releases](https://github.com/leanprover/lean4/releases) page for the current status
of each version.
v4.9.0 (development in progress)
v4.9.0
---------
* Functions defined by well-founded recursion are now marked as
`@[irreducible]`, which should prevent expensive and often unfruitful
unfolding of such definitions.
Existing proofs that hold by definitional equality (e.g. `rfl`) can be
rewritten to explictly unfold the function definition (using `simp`,
`unfold`, `rw`), or the recursive function can be temporariliy made
semireducible (using `unseal f in` before the command) or the function
definition itself can be marked as `@[semireducible]` to get the previous
behavor.
* The `MessageData.ofPPFormat` constructor has been removed.
Its functionality has been split into two:
- for lazy structured messages, please use `MessageData.lazy`;
- for embedding `Format` or `FormatWithInfos`, use `MessageData.ofFormatWithInfos`.
An example migration can be found in [#3929](https://github.com/leanprover/lean4/pull/3929/files#diff-5910592ab7452a0e1b2616c62d22202d2291a9ebb463145f198685aed6299867L109).
* The `MessageData.ofFormat` constructor has been turned into a function.
If you need to inspect `MessageData`,
you can pattern-match on `MessageData.ofFormatWithInfos`.
Development in progress.
v4.8.0
---------
* **Executables configured with `supportInterpreter := true` on Windows should now be run via `lake exe` to function properly.**
The way Lean is built on Windows has changed (see PR [#3601](https://github.com/leanprover/lean4/pull/3601)). As a result, Lake now dynamically links executables with `supportInterpreter := true` on Windows to `libleanshared.dll` and `libInit_shared.dll`. Therefore, such executables will not run unless those shared libraries are co-located with the executables or part of `PATH`. Running the executable via `lake exe` will ensure these libraries are part of `PATH`.
In a related change, the signature of the `nativeFacets` Lake configuration options has changed from a static `Array` to a function `(shouldExport : Bool) → Array`. See its docstring or Lake's [README](src/lake/README.md) for further details on the changed option.
* Lean now generates an error if the type of a theorem is **not** a proposition.
* Importing two different files containing proofs of the same theorem is no longer considered an error. This feature is particularly useful for theorems that are automatically generated on demand (e.g., equational theorems).
* Functional induction principles.
Derived from the definition of a (possibly mutually) recursive function, a **functional induction principle** is created that is tailored to proofs about that function.
For example from:
```
def ackermann : Nat → Nat → Nat
| 0, m => m + 1
| n+1, 0 => ackermann n 1
| n+1, m+1 => ackermann n (ackermann (n + 1) m)
```
we get
```
ackermann.induct (motive : Nat → Nat → Prop) (case1 : ∀ (m : Nat), motive 0 m)
(case2 : ∀ (n : Nat), motive n 1 → motive (Nat.succ n) 0)
(case3 : ∀ (n m : Nat), motive (n + 1) m → motive n (ackermann (n + 1) m) → motive (Nat.succ n) (Nat.succ m))
(x x : Nat) : motive x x
```
It can be used in the `induction` tactic using the `using` syntax:
```
induction n, m using ackermann.induct
```
* The termination checker now recognizes more recursion patterns without an
explicit `termination_by`. In particular the idiom of counting up to an upper
bound, as in
```
def Array.sum (arr : Array Nat) (i acc : Nat) : Nat :=
if _ : i < arr.size then
Array.sum arr (i+1) (acc + arr[i])
else
acc
```
is recognized without having to say `termination_by arr.size - i`.
* Shorter instances names. There is a new algorithm for generating names for anonymous instances.
Across Std and Mathlib, the median ratio between lengths of new names and of old names is about 72%.
With the old algorithm, the longest name was 1660 characters, and now the longest name is 202 characters.
The new algorithm's 95th percentile name length is 67 characters, versus 278 for the old algorithm.
While the new algorithm produces names that are 1.2% less unique,
it avoids cross-project collisions by adding a module-based suffix
when it does not refer to declarations from the same "project" (modules that share the same root).
PR [#3089](https://github.com/leanprover/lean4/pull/3089).
* Attribute `@[pp_using_anonymous_constructor]` to make structures pretty print like `⟨x, y, z⟩`
rather than `{a := x, b := y, c := z}`.
This attribute is applied to `Sigma`, `PSigma`, `PProd`, `Subtype`, `And`, and `Fin`.
* Now structure instances pretty print with parent structures' fields inlined.
That is, if `B` extends `A`, then `{ toA := { x := 1 }, y := 2 }` now pretty prints as `{ x := 1, y := 2 }`.
Setting option `pp.structureInstances.flatten` to false turns this off.
* Option `pp.structureProjections` is renamed to `pp.fieldNotation`, and there is now a suboption `pp.fieldNotation.generalized`
to enable pretty printing function applications using generalized field notation (defaults to true).
Field notation can be disabled on a function-by-function basis using the `@[pp_nodot]` attribute.
* Added options `pp.mvars` (default: true) and `pp.mvars.withType` (default: false).
When `pp.mvars` is false, expression metavariables pretty print as `?_` and universe metavariables pretty print as `_`.
When `pp.mvars.withType` is true, expression metavariables pretty print with a type ascription.
These can be set when using `#guard_msgs` to make tests not depend on the particular names of metavariables.
[#3798](https://github.com/leanprover/lean4/pull/3798) and
[#3978](https://github.com/leanprover/lean4/pull/3978).
* Hovers for terms in `match` expressions in the Infoview now reliably show the correct term.
* Added `@[induction_eliminator]` and `@[cases_eliminator]` attributes to be able to define custom eliminators
for the `induction` and `cases` tactics, replacing the `@[eliminator]` attribute.
Gives custom eliminators for `Nat` so that `induction` and `cases` put goal states into terms of `0` and `n + 1`
rather than `Nat.zero` and `Nat.succ n`.
Added option `tactic.customEliminators` to control whether to use custom eliminators.
Added a hack for `rcases`/`rintro`/`obtain` to use the custom eliminator for `Nat`.
[#3629](https://github.com/leanprover/lean4/pull/3629),
[#3655](https://github.com/leanprover/lean4/pull/3655), and
[#3747](https://github.com/leanprover/lean4/pull/3747).
* The `#guard_msgs` command now has options to change whitespace normalization and sensitivity to message ordering.
For example, `#guard_msgs (whitespace := lax) in cmd` collapses whitespace before checking messages,
and `#guard_msgs (ordering := sorted) in cmd` sorts the messages in lexicographic order before checking.
PR [#3883](https://github.com/leanprover/lean4/pull/3883).
* The `#guard_msgs` command now supports showing a diff between the expected and actual outputs. This feature is currently
disabled by default, but can be enabled with `set_option guard_msgs.diff true`. Depending on user feedback, this option
may default to `true` in a future version of Lean.
Breaking changes:
* Automatically generated equational theorems are now named using suffix `.eq_<idx>` instead of `._eq_<idx>`, and `.def` instead of `._unfold`. Example:
```
def fact : Nat → Nat
| 0 => 1
| n+1 => (n+1) * fact n
theorem ex : fact 0 = 1 := by unfold fact; decide
#check fact.eq_1
-- fact.eq_1 : fact 0 = 1
#check fact.eq_2
-- fact.eq_2 (n : Nat) : fact (Nat.succ n) = (n + 1) * fact n
#check fact.def
/-
fact.def :
∀ (x : Nat),
fact x =
match x with
| 0 => 1
| Nat.succ n => (n + 1) * fact n
-/
```
* The coercion from `String` to `Name` was removed. Previously, it was `Name.mkSimple`, which does not separate strings at dots, but experience showed that this is not always the desired coercion. For the previous behavior, manually insert a call to `Name.mkSimple`.
* The `Subarray` fields `as`, `h₁` and `h₂` have been renamed to `array`, `start_le_stop`, and `stop_le_array_size`, respectively. This more closely follows standard Lean conventions. Deprecated aliases for the field projections were added; these will be removed in a future release.
* The change to the instance name algorithm (described above) can break projects that made use of the auto-generated names.
* `Option.toMonad` has been renamed to `Option.getM` and the unneeded `[Monad m]` instance argument has been removed.
Release candidate, release notes will be copied from branch `releases/v4.8.0` once completed.
v4.7.0
---------

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
source ../../tests/common.sh
exec_check lean -j 0 -Dlinter.all=false "$f"
exec_check lean -Dlinter.all=false "$f"

View File

@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
"""
pygments.lexers.theorem
~~~~~~~~~~~~~~~~~~~~~~~
Lexers for theorem-proving languages.
:copyright: Copyright 2006-2017 by the Pygments team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
from pygments.lexer import RegexLexer, default, words
from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
Number, Punctuation, Generic
__all__ = ['Lean4Lexer']
class Lean4Lexer(RegexLexer):
"""
For the `Lean 4 <https://github.com/leanprover/lean4>`_
theorem prover.
.. versionadded:: 2.0
"""
name = 'Lean4'
aliases = ['lean4']
filenames = ['*.lean']
mimetypes = ['text/x-lean']
flags = re.MULTILINE | re.UNICODE
keywords1 = (
'import', 'abbreviation', 'opaque_hint', 'tactic_hint', 'definition',
'renaming', 'inline', 'hiding', 'parameter', 'lemma', 'variable',
'theorem', 'axiom', 'inductive', 'structure', 'universe', 'alias',
'help', 'options', 'precedence', 'postfix', 'prefix',
'infix', 'infixl', 'infixr', 'notation', '#eval',
'#check', '#reduce', '#exit', 'coercion', 'end', 'private', 'using', 'namespace',
'including', 'instance', 'section', 'context', 'protected', 'expose',
'export', 'set_option', 'extends', 'open', 'example',
'constant', 'constants', 'print', 'opaque', 'reducible', 'irreducible',
'def', 'macro', 'elab', 'syntax', 'macro_rules', 'reduce', 'where',
'abbrev', 'noncomputable', 'class', 'attribute', 'synth', 'mutual',
)
keywords2 = (
'forall', 'fun', 'Pi', 'obtain', 'from', 'have', 'show', 'assume',
'take', 'let', 'if', 'else', 'then', 'by', 'in', 'with', 'begin',
'proof', 'qed', 'calc', 'match', 'nomatch', 'do', 'at',
)
keywords3 = (
# Sorts
'Type', 'Prop', 'Sort',
)
operators = (
u'!=', u'#', u'&', u'&&', u'*', u'+', u'-', u'/', u'@', u'!', u'`',
u'-.', u'->', u'.', u'..', u'...', u'::', u':>', u';', u';;', u'<',
u'<-', u'=', u'==', u'>', u'_', u'|', u'||', u'~', u'=>', u'<=', u'>=',
u'/\\', u'\\/', u'', u'Π', u'λ', u'', u'', u'', u'', u'', u'',
u'¬', u'⁻¹', u'', u'', u'', u'', u'', u'', u'', u'×', u'',
u'', u'', u'', u'',
)
punctuation = (u'(', u')', u':', u'{', u'}', u'[', u']', u'', u'',
u':=', u',')
tokens = {
'root': [
(r'\s+', Text),
(r'/-', Comment, 'comment'),
(r'--.*?$', Comment.Single),
(words(keywords1, prefix=r'\b', suffix=r'\b'), Keyword.Namespace),
(words(keywords2, prefix=r'\b', suffix=r'\b'), Keyword),
(words(keywords3, prefix=r'\b', suffix=r'\b'), Keyword.Type),
(words(operators), Name.Builtin.Pseudo),
(words(punctuation), Operator),
(u"[A-Za-z_\u03b1-\u03ba\u03bc-\u03fb\u1f00-\u1ffe\u2100-\u214f]"
u"[A-Za-z_'\u03b1-\u03ba\u03bc-\u03fb\u1f00-\u1ffe\u2070-\u2079"
u"\u207f-\u2089\u2090-\u209c\u2100-\u214f0-9]*", Name),
(r'\d+', Number.Integer),
(r'"', String.Double, 'string'),
(r'[~?][a-z][\w\']*:', Name.Variable)
],
'comment': [
# Multiline Comments
(r'[^/-]', Comment.Multiline),
(r'/-', Comment.Multiline, '#push'),
(r'-/', Comment.Multiline, '#pop'),
(r'[/-]', Comment.Multiline)
],
'string': [
(r'[^\\"]+', String.Double),
(r'\\[n"\\]', String.Escape),
('"', String.Double, '#pop'),
],
}

View File

@@ -1,3 +1,7 @@
These are instructions to set up a working development environment for those who wish to make changes to Lean itself. It is part of the [Development Guide](doc/dev/index.md).
We strongly suggest that new users instead follow the [Quickstart](doc/quickstart.md) to get started using Lean, since this sets up an environment that can automatically manage multiple Lean toolchain versions, which is necessary when working within the Lean ecosystem.
Requirements
------------
@@ -17,39 +21,27 @@ Platform-Specific Setup
Generic Build Instructions
--------------------------
Setting up a basic release build:
Setting up a basic parallelized release build:
```bash
git clone https://github.com/leanprover/lean4 --recurse-submodules
git clone https://github.com/leanprover/lean4
cd lean4
mkdir -p build/release
cd build/release
cmake ../..
make
cmake --preset release
make -C build/release -j$(nproc) # see below for macOS
```
For regular development, we recommend running
```bash
git config submodule.recurse true
```
in the checkout so that `--recurse-submodules` doesn't have to be
specified with `git pull/checkout/...`.
You can replace `$(nproc)`, which is not available on macOS and some alternative shells, with the desired parallelism amount.
The above commands will compile the Lean library and binaries into the
`stage1` subfolder; see below for details. Add `-j N` for an
appropriate `N` to `make` for a parallel build.
`stage1` subfolder; see below for details.
For example, on an AMD Ryzen 9 `make` takes 00:04:55, whereas `make -j 10`
takes 00:01:38. Your results may vary depending on the speed of your hard
drive.
You should not usually run `make install` after a successful build.
You should not usually run `cmake --install` after a successful build.
See [Dev setup using elan](../dev/index.md#dev-setup-using-elan) on how to properly set up your editor to use the correct stage depending on the source directory.
Useful CMake Configuration Settings
-----------------------------------
Pass these along with the `cmake ../..` command.
Pass these along with the `cmake --preset release` command.
There are also two alternative presets that combine some of these options you can use instead of `release`: `debug` and `sandebug` (sanitize + debug).
* `-D CMAKE_BUILD_TYPE=`\
Select the build type. Valid values are `RELEASE` (default), `DEBUG`,

View File

@@ -1,39 +0,0 @@
# Compiling Lean with Visual Studio
WARNING: Compiling Lean with Visual Studio doesn't currently work.
There's an ongoing effort to port Lean to Visual Studio.
The instructions below are for VS 2017.
In the meantime you can use [MSYS2](msys2.md) or [WSL](wsl.md).
## Installing dependencies
First, install `vcpkg` from https://github.com/Microsoft/vcpkg if you haven't
done so already.
Then, open a console in the directory you cloned `vcpkg` to, and type:
`vcpkg install mpir` for the 32-bit library or
`vcpkg install mpir:x64-windows` for the x64 one.
In Visual Studio, use the "open folder" feature and open the Lean directory.
Go to the `CMake->Change CMake Settings` menu. File `CMakeSettings.json` opens.
In each of the targets, add the following snippet (i.e., after every
`ctestCommandArgs`):
```json
"variables": [
{
"name": "CMAKE_TOOLCHAIN_FILE",
"value": "C:\\path\\to\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake"
}
]
```
## Enable Intellisense
In Visual Studio, press Ctrl+Q and type `CppProperties.json` and press Enter.
Ensure `includePath` variables include `"${workspaceRoot}\\src"`.
## Build Lean
Press F7.

View File

@@ -38,10 +38,9 @@ cmake --version
Then follow the [generic build instructions](index.md) in the MSYS2
MinGW shell, using:
```
cmake ../.. -G "Unix Makefiles" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
cmake --preset release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
```
instead of `cmake ../..`. This ensures that cmake will call `sh` instead of `cmd.exe`
for script tasks and it will use the clang compiler instead of gcc, which is required.
instead of `cmake --preset release`. This will use the clang compiler instead of gcc, which is required with msys2.
## Install lean

View File

@@ -1,4 +1,4 @@
# Install Packages on OS X 10.9
# Install Packages on OS X 14.5
We assume that you are using [homebrew][homebrew] as a package manager.
@@ -22,7 +22,7 @@ brew install gcc
```
To install clang++-3.5 via homebrew, please execute:
```bash
brew install llvm --with-clang --with-asan
brew install llvm
```
To use compilers other than the default one (Apple's clang++), you
need to use `-DCMAKE_CXX_COMPILER` option to specify the compiler

View File

@@ -6,6 +6,7 @@ Platforms built & tested by our CI, available as binary releases via elan (see b
* x86-64 Linux with glibc 2.27+
* x86-64 macOS 10.15+
* aarch64 (Apple Silicon) macOS 10.15+
* x86-64 Windows 10+
### Tier 2
@@ -16,7 +17,6 @@ Releases may be silently broken due to the lack of automated testing.
Issue reports and fixes are welcome.
* aarch64 Linux with glibc 2.27+
* aarch64 (Apple Silicon) macOS
* x86 (32-bit) Linux
* Emscripten Web Assembly

View File

@@ -43,7 +43,8 @@ $ pdflatex test.tex
## Example with `minted`
First [install Pygments](https://pygments.org/download/). Then save [`lean4.py`](https://raw.githubusercontent.com/leanprover/lean4/master/doc/latex/lean4.py), which contains an version of the Lean highlighter updated for Lean 4, and the following sample LaTeX file `test.tex` into the same directory:
First [install Pygments](https://pygments.org/download/) (version 2.18 or newer).
Then save the following sample LaTeX file `test.tex` into the same directory:
```latex
\documentclass{article}
@@ -51,9 +52,8 @@ First [install Pygments](https://pygments.org/download/). Then save [`lean4.py`]
% switch to a monospace font supporting more Unicode characters
\setmonofont{FreeMono}
\usepackage{minted}
% instruct minted to use our local theorem.py
\newmintinline[lean]{lean4.py:Lean4Lexer -x}{bgcolor=white}
\newminted[leancode]{lean4.py:Lean4Lexer -x}{fontsize=\footnotesize}
\newmintinline[lean]{lean4}{bgcolor=white}
\newminted[leancode]{lean4}{fontsize=\footnotesize}
\usemintedstyle{tango} % a nice, colorful theme
\begin{document}
@@ -67,9 +67,6 @@ theorem funext {f₁ f₂ : ∀ (x : α), β x} (h : ∀ x, f₁ x = f₂ x) : f
\end{document}
```
If your version of `minted` is v2.7 or newer, but before v3.0,
you will additionally need to follow the workaround described in https://github.com/gpoore/minted/issues/360.
You can then compile `test.tex` by executing the following command:
```bash
@@ -81,11 +78,14 @@ Some remarks:
- either `xelatex` or `lualatex` is required to handle Unicode characters in the code.
- `--shell-escape` is needed to allow `xelatex` to execute `pygmentize` in a shell.
- If the chosen monospace font is missing some Unicode symbols, you can direct them to be displayed using a fallback font or other replacement LaTeX code.
``` latex
\usepackage{newunicodechar}
\newfontfamily{\freeserif}{DejaVu Sans}
\newunicodechar{✝}{\freeserif{✝}}
\newunicodechar{𝓞}{\ensuremath{\mathcal{O}}}
```
- minted has a "helpful" feature that draws red boxes around characters the chosen lexer doesn't recognize.
Since the Lean lexer cannot encompass all user-defined syntax, it is advisable to [work around](https://tex.stackexchange.com/a/343506/14563) this feature.
``` latex
\usepackage{newunicodechar}
\newfontfamily{\freeserif}{DejaVu Sans}
\newunicodechar{✝}{\freeserif{✝}}
\newunicodechar{𝓞}{\ensuremath{\mathcal{O}}}
```
- If you are using an old version of Pygments, you can copy
[`lean.py`](https://raw.githubusercontent.com/pygments/pygments/master/pygments/lexers/lean.py) into your working directory,
and use `lean4.py:Lean4Lexer -x` instead of `lean4` above.
If your version of `minted` is v2.7 or newer, but before v3.0,
you will additionally need to follow the workaround described in https://github.com/gpoore/minted/issues/360.

View File

@@ -170,7 +170,7 @@ rec {
ln -sf ${lean-all}/* .
'';
buildPhase = ''
ctest --output-junit test-results.xml --output-on-failure -E 'leancomptest_(doc_example|foreign)' -j$NIX_BUILD_CORES
ctest --output-junit test-results.xml --output-on-failure -E 'leancomptest_(doc_example|foreign)|leanlaketest_init' -j$NIX_BUILD_CORES
'';
installPhase = ''
mkdir $out

22
releases_drafts/README.md Normal file
View File

@@ -0,0 +1,22 @@
Draft release notes
-------------------
This folder contains drafts of release notes for inclusion in `RELEASES.md`.
During the process to create a release candidate, we look through all the commits that make up the release
to prepare the release notes, and in that process we take these drafts into account.
Guidelines:
- You should prefer adding release notes to commit messages over adding anything to this folder.
A release note should briefly explain the impact of a change from a user's point of view.
Please mark these parts out with words such as **release notes** and/or **breaking changes**.
- It is not necessary to add anything to this folder. It is meant for larger features that span multiple PRs,
or for anything that would be helpful when preparing the release notes that might be missed
by someone reading through the change log.
- If the PR that adds a feature simultaneously adds a draft release note, including the PR number is not required
since it can be obtained from the git history for the file.
When release notes are prepared, all the draft release notes are deleted from this folder.
For release candidates beyond the first one, you can either update `RELEASE.md` directly
or continue to add drafts.
When a release is finalized, we will copy the completed release notes from `RELEASE.md` to the `master` branch.

View File

@@ -0,0 +1,13 @@
* The `MessageData.ofPPFormat` constructor has been removed.
Its functionality has been split into two:
- for lazy structured messages, please use `MessageData.lazy`;
- for embedding `Format` or `FormatWithInfos`, use `MessageData.ofFormatWithInfos`.
An example migration can be found in [#3929](https://github.com/leanprover/lean4/pull/3929/files#diff-5910592ab7452a0e1b2616c62d22202d2291a9ebb463145f198685aed6299867L109).
* The `MessageData.ofFormat` constructor has been turned into a function.
If you need to inspect `MessageData`,
you can pattern-match on `MessageData.ofFormatWithInfos`.
part of #3929

12
releases_drafts/wf.md Normal file
View File

@@ -0,0 +1,12 @@
Functions defined by well-founded recursion are now marked as
`@[irreducible]`, which should prevent expensive and often unfruitful
unfolding of such definitions.
Existing proofs that hold by definitional equality (e.g. `rfl`) can be
rewritten to explictly unfold the function definition (using `simp`,
`unfold`, `rw`), or the recursive function can be temporariliy made
semireducible (using `unseal f in` before the command) or the function
definition itself can be marked as `@[semireducible]` to get the previous
behavor.
#4061

View File

@@ -534,6 +534,11 @@ def sshiftRight (a : BitVec n) (s : Nat) : BitVec n := .ofInt n (a.toInt >>> s)
instance {n} : HShiftLeft (BitVec m) (BitVec n) (BitVec m) := fun x y => x <<< y.toNat
instance {n} : HShiftRight (BitVec m) (BitVec n) (BitVec m) := fun x y => x >>> y.toNat
/-- Auxiliary function for `rotateLeft`, which does not take into account the case where
the rotation amount is greater than the bitvector width. -/
def rotateLeftAux (x : BitVec w) (n : Nat) : BitVec w :=
x <<< n ||| x >>> (w - n)
/--
Rotate left for bit vectors. All the bits of `x` are shifted to higher positions, with the top `n`
bits wrapping around to fill the low bits.
@@ -543,7 +548,15 @@ rotateLeft 0b0011#4 3 = 0b1001
```
SMT-Lib name: `rotate_left` except this operator uses a `Nat` shift amount.
-/
def rotateLeft (x : BitVec w) (n : Nat) : BitVec w := x <<< n ||| x >>> (w - n)
def rotateLeft (x : BitVec w) (n : Nat) : BitVec w := rotateLeftAux x (n % w)
/--
Auxiliary function for `rotateRight`, which does not take into account the case where
the rotation amount is greater than the bitvector width.
-/
def rotateRightAux (x : BitVec w) (n : Nat) : BitVec w :=
x >>> n ||| x <<< (w - n)
/--
Rotate right for bit vectors. All the bits of `x` are shifted to lower positions, with the
@@ -554,7 +567,7 @@ rotateRight 0b01001#5 1 = 0b10100
```
SMT-Lib name: `rotate_right` except this operator uses a `Nat` shift amount.
-/
def rotateRight (x : BitVec w) (n : Nat) : BitVec w := x >>> n ||| x <<< (w - n)
def rotateRight (x : BitVec w) (n : Nat) : BitVec w := rotateRightAux x (n % w)
/--
Concatenation of bitvectors. This uses the "big endian" convention that the more significant

View File

@@ -198,4 +198,41 @@ theorem ule_eq_not_ult (x y : BitVec w) : x.ule y = !y.ult x := by
theorem ule_eq_carry (x y : BitVec w) : x.ule y = carry w y (~~~x) true := by
simp [ule_eq_not_ult, ult_eq_not_carry]
/-- If two bitvectors have the same `msb`, then signed and unsigned comparisons coincide -/
theorem slt_eq_ult_of_msb_eq {x y : BitVec w} (h : x.msb = y.msb) :
x.slt y = x.ult y := by
simp only [BitVec.slt, toInt_eq_msb_cond, BitVec.ult, decide_eq_decide, h]
cases y.msb <;> simp
/-- If two bitvectors have different `msb`s, then unsigned comparison is determined by this bit -/
theorem ult_eq_msb_of_msb_neq {x y : BitVec w} (h : x.msb y.msb) :
x.ult y = y.msb := by
simp only [BitVec.ult, msb_eq_decide, ne_eq, decide_eq_decide] at *
omega
/-- If two bitvectors have different `msb`s, then signed and unsigned comparisons are opposites -/
theorem slt_eq_not_ult_of_msb_neq {x y : BitVec w} (h : x.msb y.msb) :
x.slt y = !x.ult y := by
simp only [BitVec.slt, toInt_eq_msb_cond, Bool.eq_not_of_ne h, ult_eq_msb_of_msb_neq h]
cases y.msb <;> (simp; omega)
theorem slt_eq_ult (x y : BitVec w) :
x.slt y = (x.msb != y.msb).xor (x.ult y) := by
by_cases h : x.msb = y.msb
· simp [h, slt_eq_ult_of_msb_eq]
· have h' : x.msb != y.msb := by simp_all
simp [slt_eq_not_ult_of_msb_neq h, h']
theorem slt_eq_not_carry (x y : BitVec w) :
x.slt y = (x.msb == y.msb).xor (carry w x (~~~y) true) := by
simp only [slt_eq_ult, bne, ult_eq_not_carry]
cases x.msb == y.msb <;> simp
theorem sle_eq_not_slt (x y : BitVec w) : x.sle y = !y.slt x := by
simp only [BitVec.sle, BitVec.slt, decide_not, decide_eq_decide]; omega
theorem sle_eq_carry (x y : BitVec w) :
x.sle y = !((x.msb == y.msb).xor (carry w y (~~~x) true)) := by
rw [sle_eq_not_slt, slt_eq_not_carry, beq_comm]
end BitVec

View File

@@ -9,6 +9,8 @@ import Init.Data.Bool
import Init.Data.BitVec.Basic
import Init.Data.Fin.Lemmas
import Init.Data.Nat.Lemmas
import Init.Data.Nat.Mod
import Init.Data.Int.Bitwise.Lemmas
namespace BitVec
@@ -140,6 +142,8 @@ theorem ofBool_eq_iff_eq : ∀(b b' : Bool), BitVec.ofBool b = BitVec.ofBool b'
@[simp, bv_toNat] theorem toNat_ofNat (x w : Nat) : (x#w).toNat = x % 2^w := by
simp [BitVec.toNat, BitVec.ofNat, Fin.ofNat']
@[simp] theorem toFin_ofNat (x : Nat) : toFin x#w = Fin.ofNat' x (Nat.two_pow_pos w) := rfl
-- Remark: we don't use `[simp]` here because simproc` subsumes it for literals.
-- If `x` and `n` are not literals, applying this theorem eagerly may not be a good idea.
theorem getLsb_ofNat (n : Nat) (x : Nat) (i : Nat) :
@@ -174,8 +178,7 @@ theorem msb_eq_getLsb_last (x : BitVec w) :
x.getLsb (w-1) = decide (2 ^ (w-1) x.toNat) := by
rcases w with rfl | w
· simp
· simp only [Nat.zero_lt_succ, decide_True, getLsb, Nat.testBit, Nat.succ_sub_succ_eq_sub,
Nat.sub_zero, Nat.and_one_is_mod, Bool.true_and, Nat.shiftRight_eq_div_pow]
· simp only [getLsb, Nat.testBit_to_div_mod, Nat.succ_sub_succ_eq_sub, Nat.sub_zero]
rcases (Nat.lt_or_ge (BitVec.toNat x) (2 ^ w)) with h | h
· simp [Nat.div_eq_of_lt h, h]
· simp only [h]
@@ -222,9 +225,21 @@ theorem toInt_eq_toNat_cond (i : BitVec n) :
if 2*i.toNat < 2^n then
(i.toNat : Int)
else
(i.toNat : Int) - (2^n : Nat) := by
unfold BitVec.toInt
split <;> omega
(i.toNat : Int) - (2^n : Nat) :=
rfl
theorem msb_eq_false_iff_two_mul_lt (x : BitVec w) : x.msb = false 2 * x.toNat < 2^w := by
cases w <;> simp [Nat.pow_succ, Nat.mul_comm _ 2, msb_eq_decide]
theorem msb_eq_true_iff_two_mul_ge (x : BitVec w) : x.msb = true 2 * x.toNat 2^w := by
simp [ Bool.ne_false_iff, msb_eq_false_iff_two_mul_lt]
/-- Characterize `x.toInt` in terms of `x.msb`. -/
theorem toInt_eq_msb_cond (x : BitVec w) :
x.toInt = if x.msb then (x.toNat : Int) - (2^w : Nat) else (x.toNat : Int) := by
simp only [BitVec.toInt, msb_eq_false_iff_two_mul_lt]
cases x.msb <;> rfl
theorem toInt_eq_toNat_bmod (x : BitVec n) : x.toInt = Int.bmod x.toNat (2^n) := by
simp only [toInt_eq_toNat_cond]
@@ -267,6 +282,9 @@ theorem toInt_ofNat {n : Nat} (x : Nat) :
have p : 0 i % (2^n : Nat) := by omega
simp [toInt_eq_toNat_bmod, Int.toNat_of_nonneg p]
@[simp] theorem ofInt_natCast (w n : Nat) :
BitVec.ofInt w (n : Int) = BitVec.ofNat w n := rfl
/-! ### zeroExtend and truncate -/
@[simp, bv_toNat] theorem toNat_zeroExtend' {m n : Nat} (p : m n) (x : BitVec m) :
@@ -449,6 +467,11 @@ protected theorem extractLsb_ofNat (x n : Nat) (hi lo : Nat) :
ext
simp
theorem or_assoc (x y z : BitVec w) :
x ||| y ||| z = x ||| (y ||| z) := by
ext i
simp [Bool.or_assoc]
/-! ### and -/
@[simp] theorem toNat_and (x y : BitVec v) :
@@ -475,6 +498,11 @@ protected theorem extractLsb_ofNat (x n : Nat) (hi lo : Nat) :
ext
simp
theorem and_assoc (x y z : BitVec w) :
x &&& y &&& z = x &&& (y &&& z) := by
ext i
simp [Bool.and_assoc]
/-! ### xor -/
@[simp] theorem toNat_xor (x y : BitVec v) :
@@ -495,6 +523,11 @@ protected theorem extractLsb_ofNat (x n : Nat) (hi lo : Nat) :
ext
simp
theorem xor_assoc (x y z : BitVec w) :
x ^^^ y ^^^ z = x ^^^ (y ^^^ z) := by
ext i
simp [Bool.xor_assoc]
/-! ### not -/
theorem not_def {x : BitVec v} : ~~~x = allOnes v ^^^ x := rfl
@@ -629,6 +662,70 @@ theorem shiftLeft_shiftLeft {w : Nat} (x : BitVec w) (n m : Nat) :
getLsb (x >>> i) j = getLsb x (i+j) := by
unfold getLsb ; simp
/-! ### sshiftRight -/
theorem sshiftRight_eq {x : BitVec n} {i : Nat} :
x.sshiftRight i = BitVec.ofInt n (x.toInt >>> i) := by
apply BitVec.eq_of_toInt_eq
simp [BitVec.sshiftRight]
/-- if the msb is false, the arithmetic shift right equals logical shift right -/
theorem sshiftRight_eq_of_msb_false {x : BitVec w} {s : Nat} (h : x.msb = false) :
(x.sshiftRight s) = x >>> s := by
apply BitVec.eq_of_toNat_eq
rw [BitVec.sshiftRight_eq, BitVec.toInt_eq_toNat_cond]
have hxbound : 2 * x.toNat < 2 ^ w := (BitVec.msb_eq_false_iff_two_mul_lt x).mp h
simp only [hxbound, reduceIte, Int.natCast_shiftRight, Int.ofNat_eq_coe, ofInt_natCast,
toNat_ofNat, toNat_ushiftRight]
replace hxbound : x.toNat >>> s < 2 ^ w := by
rw [Nat.shiftRight_eq_div_pow]
exact Nat.lt_of_le_of_lt (Nat.div_le_self ..) x.isLt
apply Nat.mod_eq_of_lt hxbound
/--
If the msb is `true`, the arithmetic shift right equals negating,
then logical shifting right, then negating again.
The double negation preserves the lower bits that have been shifted,
and the outer negation ensures that the high bits are '1'. -/
theorem sshiftRight_eq_of_msb_true {x : BitVec w} {s : Nat} (h : x.msb = true) :
(x.sshiftRight s) = ~~~((~~~x) >>> s) := by
apply BitVec.eq_of_toNat_eq
rcases w with rfl | w
· simp
· rw [BitVec.sshiftRight_eq, BitVec.toInt_eq_toNat_cond]
have hxbound : (2 * x.toNat 2 ^ (w + 1)) := (BitVec.msb_eq_true_iff_two_mul_ge x).mp h
replace hxbound : ¬ (2 * x.toNat < 2 ^ (w + 1)) := by omega
simp only [hxbound, reduceIte, toNat_ofInt, toNat_not, toNat_ushiftRight]
rw [ Int.subNatNat_eq_coe, Int.subNatNat_of_lt (by omega),
Nat.pred_eq_sub_one, Int.negSucc_shiftRight,
Int.emod_negSucc, Int.natAbs_ofNat, Nat.succ_eq_add_one,
Int.subNatNat_of_le (by omega), Int.toNat_ofNat, Nat.mod_eq_of_lt,
Nat.sub_right_comm]
omega
· rw [Nat.shiftRight_eq_div_pow]
apply Nat.lt_of_le_of_lt (Nat.div_le_self _ _) (by omega)
theorem getLsb_sshiftRight (x : BitVec w) (s i : Nat) :
getLsb (x.sshiftRight s) i =
(!decide (w i) && if s + i < w then x.getLsb (s + i) else x.msb) := by
rcases hmsb : x.msb with rfl | rfl
· simp only [sshiftRight_eq_of_msb_false hmsb, getLsb_ushiftRight, Bool.if_false_right]
by_cases hi : i w
· simp only [hi, decide_True, Bool.not_true, Bool.false_and]
apply getLsb_ge
omega
· simp only [hi, decide_False, Bool.not_false, Bool.true_and, Bool.iff_and_self,
decide_eq_true_eq]
intros hlsb
apply BitVec.lt_of_getLsb _ _ hlsb
· by_cases hi : i w
· simp [hi]
· simp only [sshiftRight_eq_of_msb_true hmsb, getLsb_not, getLsb_ushiftRight, Bool.not_and,
Bool.not_not, hi, decide_False, Bool.not_false, Bool.if_true_right, Bool.true_and,
Bool.and_iff_right_iff_imp, Bool.or_eq_true, Bool.not_eq_true', decide_eq_false_iff_not,
Nat.not_lt, decide_eq_true_eq]
omega
/-! ### append -/
theorem append_def (x : BitVec v) (y : BitVec w) :
@@ -912,6 +1009,10 @@ theorem ofNat_sub_ofNat {n} (x y : Nat) : x#n - y#n = .ofNat n (x + (2^n - y % 2
@[simp, bv_toNat] theorem toNat_neg (x : BitVec n) : (- x).toNat = (2^n - x.toNat) % 2^n := by
simp [Neg.neg, BitVec.neg]
@[simp] theorem toFin_neg (x : BitVec n) :
(-x).toFin = Fin.ofNat' (2^n - x.toNat) (Nat.two_pow_pos _) :=
rfl
theorem sub_toAdd {n} (x y : BitVec n) : x - y = x + - y := by
apply eq_of_toNat_eq
simp
@@ -1043,4 +1144,171 @@ theorem toNat_intMax_eq : (intMax w).toNat = 2^w - 1 := by
(ofBoolListLE bs).getMsb i = (decide (i < bs.length) && bs.getD (bs.length - 1 - i) false) := by
simp [getMsb_eq_getLsb]
/-! # Rotate Left -/
/-- rotateLeft is invariant under `mod` by the bitwidth. -/
@[simp]
theorem rotateLeft_mod_eq_rotateLeft {x : BitVec w} {r : Nat} :
x.rotateLeft (r % w) = x.rotateLeft r := by
simp only [rotateLeft, Nat.mod_mod]
/-- `rotateLeft` equals the bit fiddling definition of `rotateLeftAux` when the rotation amount is
smaller than the bitwidth. -/
theorem rotateLeft_eq_rotateLeftAux_of_lt {x : BitVec w} {r : Nat} (hr : r < w) :
x.rotateLeft r = x.rotateLeftAux r := by
simp only [rotateLeft, Nat.mod_eq_of_lt hr]
/--
Accessing bits in `x.rotateLeft r` the range `[0, r)` is equal to
accessing bits `x` in the range `[w - r, w)`.
Proof by example:
Let x := <6 5 4 3 2 1 0> : BitVec 7.
x.rotateLeft 2 = (<6 5 | 4 3 2 1 0>).rotateLeft 2 = <3 2 1 0 | 6 5>
(x.rotateLeft 2).getLsb ⟨i, i < 2⟩
= <3 2 1 0 | 6 5>.getLsb ⟨i, i < 2⟩
= <6 5>[i]
= <6 5 | 4 3 2 1 0>[i + len(<4 3 2 1 0>)]
= <6 5 | 4 3 2 1 0>[i + 7 - 2]
-/
theorem getLsb_rotateLeftAux_of_le {x : BitVec w} {r : Nat} {i : Nat} (hi : i < r) :
(x.rotateLeftAux r).getLsb i = x.getLsb (w - r + i) := by
rw [rotateLeftAux, getLsb_or, getLsb_ushiftRight]
suffices (x <<< r).getLsb i = false by
simp; omega
simp only [getLsb_shiftLeft, Bool.and_eq_false_imp, Bool.and_eq_true, decide_eq_true_eq,
Bool.not_eq_true', decide_eq_false_iff_not, Nat.not_lt, and_imp]
omega
/--
Accessing bits in `x.rotateLeft r` the range `[r, w)` is equal to
accessing bits `x` in the range `[0, w - r)`.
Proof by example:
Let x := <6 5 4 3 2 1 0> : BitVec 7.
x.rotateLeft 2 = (<6 5 | 4 3 2 1 0>).rotateLeft 2 = <3 2 1 0 | 6 5>
(x.rotateLeft 2).getLsb ⟨i, i ≥ 2⟩
= <3 2 1 0 | 6 5>.getLsb ⟨i, i ≥ 2⟩
= <3 2 1 0>[i - 2]
= <6 5 | 3 2 1 0>[i - 2]
Intuitively, grab the full width (7), then move the marker `|` by `r` to the right `(-2)`
Then, access the bit at `i` from the right `(+i)`.
-/
theorem getLsb_rotateLeftAux_of_geq {x : BitVec w} {r : Nat} {i : Nat} (hi : i r) :
(x.rotateLeftAux r).getLsb i = (decide (i < w) && x.getLsb (i - r)) := by
rw [rotateLeftAux, getLsb_or]
suffices (x >>> (w - r)).getLsb i = false by
have hiltr : decide (i < r) = false := by
simp [hi]
simp [getLsb_shiftLeft, Bool.or_false, hi, hiltr, this]
simp only [getLsb_ushiftRight]
apply getLsb_ge
omega
/-- When `r < w`, we give a formula for `(x.rotateRight r).getLsb i`. -/
theorem getLsb_rotateLeft_of_le {x : BitVec w} {r i : Nat} (hr: r < w) :
(x.rotateLeft r).getLsb i =
cond (i < r)
(x.getLsb (w - r + i))
(decide (i < w) && x.getLsb (i - r)) := by
· rw [rotateLeft_eq_rotateLeftAux_of_lt hr]
by_cases h : i < r
· simp [h, getLsb_rotateLeftAux_of_le h]
· simp [h, getLsb_rotateLeftAux_of_geq <| Nat.ge_of_not_lt h]
@[simp]
theorem getLsb_rotateLeft {x : BitVec w} {r i : Nat} :
(x.rotateLeft r).getLsb i =
cond (i < r % w)
(x.getLsb (w - (r % w) + i))
(decide (i < w) && x.getLsb (i - (r % w))) := by
rcases w with rfl, w
· simp
· rw [ rotateLeft_mod_eq_rotateLeft, getLsb_rotateLeft_of_le (Nat.mod_lt _ (by omega))]
/-! ## Rotate Right -/
/--
Accessing bits in `x.rotateRight r` the range `[0, w-r)` is equal to
accessing bits `x` in the range `[r, w)`.
Proof by example:
Let x := <6 5 4 3 2 1 0> : BitVec 7.
x.rotateRight 2 = (<6 5 4 3 2 | 1 0>).rotateRight 2 = <1 0 | 6 5 4 3 2>
(x.rotateLeft 2).getLsb ⟨i, i ≤ 7 - 2⟩
= <1 0 | 6 5 4 3 2>.getLsb ⟨i, i ≤ 7 - 2⟩
= <6 5 4 3 2>.getLsb i
= <6 5 4 3 2 | 1 0>[i + 2]
-/
theorem getLsb_rotateRightAux_of_le {x : BitVec w} {r : Nat} {i : Nat} (hi : i < w - r) :
(x.rotateRightAux r).getLsb i = x.getLsb (r + i) := by
rw [rotateRightAux, getLsb_or, getLsb_ushiftRight]
suffices (x <<< (w - r)).getLsb i = false by
simp only [this, Bool.or_false]
simp only [getLsb_shiftLeft, Bool.and_eq_false_imp, Bool.and_eq_true, decide_eq_true_eq,
Bool.not_eq_true', decide_eq_false_iff_not, Nat.not_lt, and_imp]
omega
/--
Accessing bits in `x.rotateRight r` the range `[w-r, w)` is equal to
accessing bits `x` in the range `[0, r)`.
Proof by example:
Let x := <6 5 4 3 2 1 0> : BitVec 7.
x.rotateRight 2 = (<6 5 4 3 2 | 1 0>).rotateRight 2 = <1 0 | 6 5 4 3 2>
(x.rotateLeft 2).getLsb ⟨i, i ≥ 7 - 2⟩
= <1 0 | 6 5 4 3 2>.getLsb ⟨i, i ≤ 7 - 2⟩
= <1 0>.getLsb (i - len(<6 5 4 3 2>)
= <6 5 4 3 2 | 1 0> (i - len<6 4 4 3 2>)
-/
theorem getLsb_rotateRightAux_of_geq {x : BitVec w} {r : Nat} {i : Nat} (hi : i w - r) :
(x.rotateRightAux r).getLsb i = (decide (i < w) && x.getLsb (i - (w - r))) := by
rw [rotateRightAux, getLsb_or]
suffices (x >>> r).getLsb i = false by
simp only [this, getLsb_shiftLeft, Bool.false_or]
by_cases hiw : i < w
<;> simp [hiw, hi]
simp only [getLsb_ushiftRight]
apply getLsb_ge
omega
/-- `rotateRight` equals the bit fiddling definition of `rotateRightAux` when the rotation amount is
smaller than the bitwidth. -/
theorem rotateRight_eq_rotateRightAux_of_lt {x : BitVec w} {r : Nat} (hr : r < w) :
x.rotateRight r = x.rotateRightAux r := by
simp only [rotateRight, Nat.mod_eq_of_lt hr]
/-- rotateRight is invariant under `mod` by the bitwidth. -/
@[simp]
theorem rotateRight_mod_eq_rotateRight {x : BitVec w} {r : Nat} :
x.rotateRight (r % w) = x.rotateRight r := by
simp only [rotateRight, Nat.mod_mod]
/-- When `r < w`, we give a formula for `(x.rotateRight r).getLsb i`. -/
theorem getLsb_rotateRight_of_le {x : BitVec w} {r i : Nat} (hr: r < w) :
(x.rotateRight r).getLsb i =
cond (i < w - r)
(x.getLsb (r + i))
(decide (i < w) && x.getLsb (i - (w - r))) := by
· rw [rotateRight_eq_rotateRightAux_of_lt hr]
by_cases h : i < w - r
· simp [h, getLsb_rotateRightAux_of_le h]
· simp [h, getLsb_rotateRightAux_of_geq <| Nat.le_of_not_lt h]
@[simp]
theorem getLsb_rotateRight {x : BitVec w} {r i : Nat} :
(x.rotateRight r).getLsb i =
cond (i < w - (r % w))
(x.getLsb ((r % w) + i))
(decide (i < w) && x.getLsb (i - (w - (r % w)))) := by
rcases w with rfl, w
· simp
· rw [ rotateRight_mod_eq_rotateRight, getLsb_rotateRight_of_le (Nat.mod_lt _ (by omega))]
end BitVec

View File

@@ -227,6 +227,8 @@ instance : Std.Associative (· != ·) := ⟨bne_assoc⟩
@[simp] theorem bne_left_inj : (x y z : Bool), (x != y) = (x != z) y = z := by decide
@[simp] theorem bne_right_inj : (x y z : Bool), (x != z) = (y != z) x = y := by decide
theorem eq_not_of_ne : {x y : Bool}, x y x = !y := by decide
/-! ### coercision related normal forms -/
theorem beq_eq_decide_eq [BEq α] [LawfulBEq α] [DecidableEq α] (a b : α) :

View File

@@ -5,3 +5,4 @@ Authors: Leonardo de Moura
-/
prelude
import Init.Data.Char.Basic
import Init.Data.Char.Lemmas

View File

@@ -0,0 +1,25 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Init.Data.Char.Basic
import Init.Data.UInt.Lemmas
namespace Char
theorem le_def {a b : Char} : a b a.1 b.1 := .rfl
theorem lt_def {a b : Char} : a < b a.1 < b.1 := .rfl
theorem lt_iff_val_lt_val {a b : Char} : a < b a.val < b.val := Iff.rfl
@[simp] protected theorem not_le {a b : Char} : ¬ a b b < a := UInt32.not_le
@[simp] protected theorem not_lt {a b : Char} : ¬ a < b b a := UInt32.not_lt
@[simp] protected theorem le_refl (a : Char) : a a := by simp [le_def]
@[simp] protected theorem lt_irrefl (a : Char) : ¬ a < a := by simp
protected theorem le_trans {a b c : Char} : a b b c a c := UInt32.le_trans
protected theorem lt_trans {a b c : Char} : a < b b < c a < c := UInt32.lt_trans
protected theorem le_total (a b : Char) : a b b a := UInt32.le_total a.1 b.1
protected theorem lt_asymm {a b : Char} (h : a < b) : ¬ b < a := UInt32.lt_asymm h
protected theorem ne_of_lt {a b : Char} (h : a < b) : a b := Char.ne_of_val_ne (UInt32.ne_of_lt h)
end Char

View File

@@ -6,6 +6,8 @@ Authors: François G. Dorais
prelude
import Init.Data.Nat.Linear
namespace Fin
/-- Folds over `Fin n` from the left: `foldl 3 f x = f (f (f x 0) 1) 2`. -/
@[inline] def foldl (n) (f : α Fin n α) (init : α) : α := loop init 0 where
/-- Inner loop for `Fin.foldl`. `Fin.foldl.loop n f x i = f (f (f x i) ...) (n-1)` -/
@@ -20,3 +22,5 @@ import Init.Data.Nat.Linear
loop : {i // i n} α α
| 0, _, x => x
| i+1, h, x => loop i, Nat.le_of_lt h (f i, h x)
end Fin

View File

@@ -1,7 +1,7 @@
/-
Copyright (c) 2022 Mario Carneiro. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mario Carneiro
Authors: Mario Carneiro, Leonardo de Moura
-/
prelude
import Init.Data.Fin.Basic
@@ -94,6 +94,18 @@ theorem lt_iff_val_lt_val {a b : Fin n} : a < b ↔ a.val < b.val := Iff.rfl
@[simp] protected theorem not_lt {a b : Fin n} : ¬ a < b b a := Nat.not_lt
@[simp] protected theorem le_refl (a : Fin n) : a a := by simp [le_def]
@[simp] protected theorem lt_irrefl (a : Fin n) : ¬ a < a := by simp
protected theorem le_trans {a b c : Fin n} : a b b c a c := Nat.le_trans
protected theorem lt_trans {a b c : Fin n} : a < b b < c a < c := Nat.lt_trans
protected theorem le_total (a b : Fin n) : a b b a := Nat.le_total a b
protected theorem lt_asymm {a b : Fin n} (h : a < b) : ¬ b < a := Nat.lt_asymm h
protected theorem ne_of_lt {a b : Fin n} (h : a < b) : a b := Fin.ne_of_val_ne (Nat.ne_of_lt h)
protected theorem ne_of_gt {a b : Fin n} (h : a < b) : b a := Fin.ne_of_val_ne (Nat.ne_of_gt h)
@@ -823,27 +835,3 @@ protected theorem zero_mul (k : Fin (n + 1)) : (0 : Fin (n + 1)) * k = 0 := by
simp [ext_iff, mul_def]
end Fin
namespace USize
@[simp] theorem lt_def {a b : USize} : a < b a.toNat < b.toNat := .rfl
@[simp] theorem le_def {a b : USize} : a b a.toNat b.toNat := .rfl
@[simp] theorem zero_toNat : (0 : USize).toNat = 0 := Nat.zero_mod _
@[simp] theorem mod_toNat (a b : USize) : (a % b).toNat = a.toNat % b.toNat :=
Fin.mod_val ..
@[simp] theorem div_toNat (a b : USize) : (a / b).toNat = a.toNat / b.toNat :=
Fin.div_val ..
@[simp] theorem modn_toNat (a : USize) (b : Nat) : (a.modn b).toNat = a.toNat % b :=
Fin.modn_val ..
theorem mod_lt (a b : USize) (h : 0 < b) : a % b < b := USize.modn_lt _ (by simp at h; exact h)
theorem toNat.inj : {a b : USize}, a.toNat = b.toNat a = b
| _, _, _, _, rfl => rfl
end USize

View File

@@ -0,0 +1,37 @@
/-
Copyright (c) 2023 Siddharth Bhat. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Siddharth Bhat, Jeremy Avigad
-/
prelude
import Init.Data.Nat.Bitwise.Lemmas
import Init.Data.Int.Bitwise
namespace Int
theorem shiftRight_eq (n : Int) (s : Nat) : n >>> s = Int.shiftRight n s := rfl
@[simp]
theorem natCast_shiftRight (n s : Nat) : (n : Int) >>> s = n >>> s := rfl
@[simp]
theorem negSucc_shiftRight (m n : Nat) :
-[m+1] >>> n = -[m >>>n +1] := rfl
theorem shiftRight_add (i : Int) (m n : Nat) :
i >>> (m + n) = i >>> m >>> n := by
simp only [shiftRight_eq, Int.shiftRight]
cases i <;> simp [Nat.shiftRight_add]
theorem shiftRight_eq_div_pow (m : Int) (n : Nat) :
m >>> n = m / ((2 ^ n) : Nat) := by
simp only [shiftRight_eq, Int.shiftRight, Nat.shiftRight_eq_div_pow]
split
· simp
· rw [negSucc_ediv _ (by norm_cast; exact Nat.pow_pos (Nat.zero_lt_two))]
rfl
@[simp]
theorem zero_shiftRight (n : Nat) : (0 : Int) >>> n = 0 := by
simp [Int.shiftRight_eq_div_pow]
end Int

View File

@@ -420,6 +420,9 @@ theorem negSucc_emod (m : Nat) {b : Int} (bpos : 0 < b) : -[m+1] % b = b - 1 - m
match b, eq_succ_of_zero_lt bpos with
| _, n, rfl => rfl
theorem emod_negSucc (m : Nat) (n : Int) :
(Int.negSucc m) % n = Int.subNatNat (Int.natAbs n) (Nat.succ (m % Int.natAbs n)) := rfl
theorem ofNat_mod_ofNat (m n : Nat) : (m % n : Int) = (m % n) := rfl
theorem emod_nonneg : (a : Int) {b : Int}, b 0 0 a % b

View File

@@ -96,7 +96,7 @@ protected theorem le_antisymm {a b : Int} (h₁ : a ≤ b) (h₂ : b ≤ a) : a
have := Int.ofNat.inj <| Int.add_left_cancel <| this.trans (Int.add_zero _).symm
rw [ hn, Nat.eq_zero_of_add_eq_zero_left this, ofNat_zero, Int.add_zero a]
protected theorem lt_irrefl (a : Int) : ¬a < a := fun H =>
@[simp] protected theorem lt_irrefl (a : Int) : ¬a < a := fun H =>
let n, hn := lt.dest H
have : (a+Nat.succ n) = a+0 := by
rw [hn, Int.add_zero]
@@ -813,6 +813,20 @@ protected theorem sub_lt_sub_right {a b : Int} (h : a < b) (c : Int) : a - c < b
protected theorem sub_lt_sub {a b c d : Int} (hab : a < b) (hcd : c < d) : a - d < b - c :=
Int.add_lt_add hab (Int.neg_lt_neg hcd)
protected theorem lt_of_sub_lt_sub_left {a b c : Int} (h : c - a < c - b) : b < a :=
Int.lt_of_neg_lt_neg <| Int.lt_of_add_lt_add_left h
protected theorem lt_of_sub_lt_sub_right {a b c : Int} (h : a - c < b - c) : a < b :=
Int.lt_of_add_lt_add_right h
@[simp] protected theorem sub_lt_sub_left_iff (a b c : Int) :
c - a < c - b b < a :=
Int.lt_of_sub_lt_sub_left, (Int.sub_lt_sub_left · c)
@[simp] protected theorem sub_lt_sub_right_iff (a b c : Int) :
a - c < b - c a < b :=
Int.lt_of_sub_lt_sub_right, (Int.sub_lt_sub_right · c)
protected theorem sub_lt_sub_of_le_of_lt {a b c d : Int}
(hab : a b) (hcd : c < d) : a - d < b - c :=
Int.add_lt_add_of_le_of_lt hab (Int.neg_lt_neg hcd)

View File

@@ -78,6 +78,8 @@ of a number.
-/
/-- `testBit m n` returns whether the `(n+1)` least significant bit is `1` or `0`-/
def testBit (m n : Nat) : Bool := (m >>> n) &&& 1 != 0
def testBit (m n : Nat) : Bool :=
-- `1 &&& n` is faster than `n &&& 1` for big `n`.
1 &&& (m >>> n) != 0
end Nat

View File

@@ -60,6 +60,13 @@ noncomputable def div2Induction {motive : Nat → Sort u}
unfold bitwise
simp
@[simp] theorem one_and_eq_mod_two (n : Nat) : 1 &&& n = n % 2 := by
if n0 : n = 0 then
subst n0; decide
else
simp only [HAnd.hAnd, AndOp.and, land]
cases mod_two_eq_zero_or_one n with | _ h => simp [bitwise, n0, h]
@[simp] theorem and_one_is_mod (x : Nat) : x &&& 1 = x % 2 := by
if xz : x = 0 then
simp [xz, zero_and]
@@ -74,7 +81,7 @@ noncomputable def div2Induction {motive : Nat → Sort u}
/-! ### testBit -/
@[simp] theorem zero_testBit (i : Nat) : testBit 0 i = false := by
simp only [testBit, zero_shiftRight, zero_and, bne_self_eq_false]
simp only [testBit, zero_shiftRight, and_zero, bne_self_eq_false]
@[simp] theorem testBit_zero (x : Nat) : testBit x 0 = decide (x % 2 = 1) := by
cases mod_two_eq_zero_or_one x with | _ p => simp [testBit, p]

View File

@@ -794,6 +794,9 @@ theorem shiftLeft_shiftLeft (m n : Nat) : ∀ k, (m <<< n) <<< k = m <<< (n + k)
| 0 => rfl
| k + 1 => by simp [ Nat.add_assoc, shiftLeft_shiftLeft _ _ k, shiftLeft_succ]
@[simp] theorem shiftLeft_shiftRight (x n : Nat) : x <<< n >>> n = x := by
rw [Nat.shiftLeft_eq, Nat.shiftRight_eq_div_pow, Nat.mul_div_cancel _ (Nat.two_pow_pos _)]
theorem mul_add_div {m : Nat} (m_pos : m > 0) (x y : Nat) : (m * x + y) / m = x + y / m := by
match x with
| 0 => simp

View File

@@ -714,4 +714,10 @@ theorem Expr.eq_of_toNormPoly_eq (ctx : Context) (e e' : Expr) (h : e.toNormPoly
simp [Expr.toNormPoly, Poly.norm] at h
assumption
end Nat.Linear
end Linear
def elimOffset {α : Sort u} (a b k : Nat) (h₁ : a + k = b + k) (h₂ : a = b α) : α := by
simp_arith at h₁
exact h₂ h₁
end Nat

View File

@@ -101,7 +101,7 @@ theorem ball_ne_none {p : Option α → Prop} : (∀ x (_ : x ≠ none), p x)
@[simp] theorem bind_none (x : Option α) : x.bind (fun _ => none (α := β)) = none := by
cases x <;> rfl
@[simp] theorem bind_eq_some : x.bind f = some b a, x = some a f a = some b := by
theorem bind_eq_some : x.bind f = some b a, x = some a f a = some b := by
cases x <;> simp
@[simp] theorem bind_eq_none {o : Option α} {f : α Option β} :
@@ -119,7 +119,7 @@ theorem bind_assoc (x : Option α) (f : α → Option β) (g : β → Option γ)
(x.bind f).bind g = x.bind fun y => (f y).bind g := by cases x <;> rfl
theorem join_eq_some : x.join = some a x = some (some a) := by
simp
simp [bind_eq_some]
theorem join_ne_none : x.join none z, x = some (some z) := by
simp only [ne_none_iff_exists', join_eq_some, iff_self]

View File

@@ -6,3 +6,4 @@ Authors: Leonardo de Moura
prelude
import Init.Data.String.Basic
import Init.Data.String.Extra
import Init.Data.String.Lemmas

View File

@@ -186,8 +186,9 @@ Returns the next position in a string after position `p`. If `p` is not a valid
the result is unspecified.
Examples:
* `"abc".next ⟨1⟩ = String.Pos.mk 2`
* `"L∃∀N".next ⟨1⟩ = String.Pos.mk 4`, since `'∃'` is a multi-byte UTF-8 character
Given `def abc := "abc"` and `def lean := "L∃∀N"`,
* `abc.get (0 |> abc.next) = 'b'`
* `lean.get (0 |> lean.next |> lean.next) = '∀'`
Cases where the result is unspecified:
* `"abc".next ⟨3⟩`, since `3 = s.endPos`
@@ -204,16 +205,52 @@ def utf8PrevAux : List Char → Pos → Pos → Pos
let i' := i + c
if i' = p then i else utf8PrevAux cs i' p
/--
Returns the position in a string before a specified position, `p`. If `p = ⟨0⟩`, returns `0`.
If `p` is not a valid position, the result is unspecified.
Examples:
Given `def abc := "abc"` and `def lean := "L∃∀N"`,
* `abc.get (abc.endPos |> abc.prev) = 'c'`
* `lean.get (lean.endPos |> lean.prev |> lean.prev |> lean.prev) = '∃'`
* `"L∃∀N".prev ⟨3⟩` is unspecified, since byte 3 occurs in the middle of the multi-byte character `'∃'`.
-/
@[extern "lean_string_utf8_prev"]
def prev : (@& String) (@& Pos) Pos
| s, p => if p = 0 then 0 else utf8PrevAux s 0 p
/--
Returns the first character in `s`. If `s = ""`, returns `(default : Char)`.
Examples:
* `"abc".front = 'a'`
* `"".front = (default : Char)`
-/
def front (s : String) : Char :=
get s 0
/--
Returns the last character in `s`. If `s = ""`, returns `(default : Char)`.
Examples:
* `"abc".back = 'c'`
* `"".back = (default : Char)`
-/
def back (s : String) : Char :=
get s (prev s s.endPos)
/--
Returns `true` if a specified position is greater than or equal to the position which
points to the end of a string. Otherwise, returns `false`.
Examples:
Given `def abc := "abc"` and `def lean := "L∃∀N"`,
* `(0 |> abc.next |> abc.next |> abc.atEnd) = false`
* `(0 |> abc.next |> abc.next |> abc.next |> abc.next |> abc.atEnd) = true`
* `(0 |> lean.next |> lean.next |> lean.next |> lean.next |> lean.atEnd) = true`
Because `"L∃∀N"` contains multi-byte characters, `lean.next (lean.next 0)` is not equal to `abc.next (abc.next 0)`.
-/
@[extern "lean_string_utf8_at_end"]
def atEnd : (@& String) (@& Pos) Bool
| s, p => p.byteIdx utf8ByteSize s
@@ -914,6 +951,10 @@ def beq (ss1 ss2 : Substring) : Bool :=
instance hasBeq : BEq Substring := beq
/-- Checks whether two substrings have the same position and content. -/
def sameAs (ss1 ss2 : Substring) : Bool :=
ss1.startPos == ss2.startPos && ss1 == ss2
end Substring
namespace String

View File

@@ -198,4 +198,35 @@ def removeLeadingSpaces (s : String) : String :=
let n := findLeadingSpacesSize s
if n == 0 then s else removeNumLeadingSpaces n s
/--
Replaces each `\r\n` with `\n` to normalize line endings,
but does not validate that there are no isolated `\r` characters.
It is an optimized version of `String.replace text "\r\n" "\n"`.
-/
def crlfToLf (text : String) : String :=
go "" 0 0
where
go (acc : String) (accStop pos : String.Pos) : String :=
if h : text.atEnd pos then
-- note: if accStop = 0 then acc is empty
if accStop = 0 then text else acc ++ text.extract accStop pos
else
let c := text.get' pos h
let pos' := text.next' pos h
if h' : ¬ text.atEnd pos' c == '\r' text.get pos' == '\n' then
let acc := acc ++ text.extract accStop pos
go acc pos' (text.next' pos' h'.1)
else
go acc accStop pos'
termination_by text.utf8ByteSize - pos.byteIdx
decreasing_by
decreasing_with
show text.utf8ByteSize - (text.next' (text.next' pos _) _).byteIdx < text.utf8ByteSize - pos.byteIdx
have k := Nat.gt_of_not_le <| mt decide_eq_true h
exact Nat.sub_lt_sub_left k (Nat.lt_trans (String.lt_next text pos) (String.lt_next _ _))
decreasing_with
show text.utf8ByteSize - (text.next' pos _).byteIdx < text.utf8ByteSize - pos.byteIdx
have k := Nat.gt_of_not_le <| mt decide_eq_true h
exact Nat.sub_lt_sub_left k (String.lt_next _ _)
end String

View File

@@ -0,0 +1,21 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Init.Data.Char.Lemmas
namespace String
protected theorem data_eq_of_eq {a b : String} (h : a = b) : a.data = b.data :=
h rfl
protected theorem ne_of_data_ne {a b : String} (h : a.data b.data) : a b :=
fun h' => absurd (String.data_eq_of_eq h') h
@[simp] protected theorem lt_irrefl (s : String) : ¬ s < s :=
List.lt_irrefl' Char.lt_irrefl s.data
protected theorem ne_of_lt {a b : String} (h : a < b) : a b := by
have := String.lt_irrefl a
intro h; subst h; contradiction
end String

View File

@@ -6,3 +6,4 @@ Authors: Henrik Böving
prelude
import Init.Data.UInt.Basic
import Init.Data.UInt.Log2
import Init.Data.UInt.Lemmas

View File

@@ -364,6 +364,3 @@ instance (a b : USize) : Decidable (a < b) := USize.decLt a b
instance (a b : USize) : Decidable (a b) := USize.decLe a b
instance : Max USize := maxOfLe
instance : Min USize := minOfLe
theorem USize.modn_lt {m : Nat} : (u : USize), m > 0 USize.toNat (u % m) < m
| u, h => Fin.modn_lt u h

View File

@@ -0,0 +1,66 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Init.Data.UInt.Basic
import Init.Data.Fin.Lemmas
set_option hygiene false in
macro "declare_uint_theorems" typeName:ident : command =>
`(
namespace $typeName
instance : Inhabited $typeName where
default := 0
theorem zero_def : (0 : $typeName) = 0 := rfl
theorem one_def : (1 : $typeName) = 1 := rfl
theorem sub_def (a b : $typeName) : a - b = a.val - b.val := rfl
theorem mul_def (a b : $typeName) : a * b = a.val * b.val := rfl
theorem mod_def (a b : $typeName) : a % b = a.val % b.val := rfl
theorem add_def (a b : $typeName) : a + b = a.val + b.val := rfl
@[simp] theorem mk_val_eq : (a : $typeName), mk a.val = a
| _, _ => rfl
theorem val_eq_of_lt {a : Nat} : a < size ((ofNat a).val : Nat) = a :=
Nat.mod_eq_of_lt
theorem le_def {a b : $typeName} : a b a.1 b.1 := .rfl
theorem lt_def {a b : $typeName} : a < b a.1 < b.1 := .rfl
theorem lt_iff_val_lt_val {a b : $typeName} : a < b a.val < b.val := .rfl
@[simp] protected theorem not_le {a b : $typeName} : ¬ a b b < a := Fin.not_le
@[simp] protected theorem not_lt {a b : $typeName} : ¬ a < b b a := Fin.not_lt
@[simp] protected theorem le_refl (a : $typeName) : a a := by simp [le_def]
@[simp] protected theorem lt_irrefl (a : $typeName) : ¬ a < a := by simp
protected theorem le_trans {a b c : $typeName} : a b b c a c := Fin.le_trans
protected theorem lt_trans {a b c : $typeName} : a < b b < c a < c := Fin.lt_trans
protected theorem le_total (a b : $typeName) : a b b a := Fin.le_total a.1 b.1
protected theorem lt_asymm {a b : $typeName} (h : a < b) : ¬ b < a := Fin.lt_asymm h
protected theorem val_eq_of_eq {a b : $typeName} (h : a = b) : a.val = b.val := h rfl
protected theorem eq_of_val_eq {a b : $typeName} (h : a.val = b.val) : a = b := by cases a; cases b; simp at h; simp [h]
open $typeName (val_eq_of_eq) in
protected theorem ne_of_val_ne {a b : $typeName} (h : a.val b.val) : a b := fun h' => absurd (val_eq_of_eq h') h
open $typeName (ne_of_val_ne) in
protected theorem ne_of_lt {a b : $typeName} (h : a < b) : a b := ne_of_val_ne (Fin.ne_of_lt h)
@[simp] protected theorem zero_toNat : (0 : $typeName).toNat = 0 := Nat.zero_mod _
@[simp] protected theorem mod_toNat (a b : $typeName) : (a % b).toNat = a.toNat % b.toNat := Fin.mod_val ..
@[simp] protected theorem div_toNat (a b : $typeName) : (a / b).toNat = a.toNat / b.toNat := Fin.div_val ..
@[simp] protected theorem modn_toNat (a : $typeName) (b : Nat) : (a.modn b).toNat = a.toNat % b := Fin.modn_val ..
protected theorem modn_lt {m : Nat} : (u : $typeName), m > 0 toNat (u % m) < m
| u, h => Fin.modn_lt u h
open $typeName (modn_lt) in
protected theorem mod_lt (a b : $typeName) (h : 0 < b) : a % b < b := modn_lt _ (by simp [lt_def] at h; exact h)
protected theorem toNat.inj : {a b : $typeName}, a.toNat = b.toNat a = b
| _, _, _, _, rfl => rfl
end $typeName
)
declare_uint_theorems UInt8
declare_uint_theorems UInt16
declare_uint_theorems UInt32
declare_uint_theorems UInt64
declare_uint_theorems USize

View File

@@ -7,3 +7,4 @@ prelude
import Init.Grind.Norm
import Init.Grind.Tactics
import Init.Grind.Lemmas
import Init.Grind.Cases

15
src/Init/Grind/Cases.lean Normal file
View File

@@ -0,0 +1,15 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Init.Core
attribute [grind_cases] And Prod False Empty True Unit Exists
namespace Lean.Grind.Eager
attribute [scoped grind_cases] Or
end Lean.Grind.Eager

View File

@@ -7,8 +7,19 @@ prelude
import Init.Tactics
namespace Lean.Grind
/--
The configuration for `grind`.
Passed to `grind` using, for example, the `grind (config := { eager := true })` syntax.
-/
structure Config where
/--
When `eager` is true (default: `false`), `grind` eagerly splits `if-then-else` and `match`
expressions.
-/
eager : Bool := false
deriving Inhabited, BEq
/-!
`grind` tactic and related tactics.
-/
end Lean.Grind

View File

@@ -3644,6 +3644,17 @@ def getPos? (info : SourceInfo) (canonicalOnly := false) : Option String.Pos :=
| synthetic (pos := pos) .., false => some pos
| _, _ => none
/--
Gets the end position information from a `SourceInfo`, if available.
If `originalOnly` is true, then `.synthetic` syntax will also return `none`.
-/
def getTailPos? (info : SourceInfo) (canonicalOnly := false) : Option String.Pos :=
match info, canonicalOnly with
| original (endPos := endPos) .., _
| synthetic (endPos := endPos) (canonical := true) .., _
| synthetic (endPos := endPos) .., false => some endPos
| _, _ => none
end SourceInfo
/--

View File

@@ -264,6 +264,13 @@ local macro "nonempty_list" : tactic =>
/-- Helper method for implementing "deterministic" timeouts. It is the number of "small" memory allocations performed by the current execution thread. -/
@[extern "lean_io_get_num_heartbeats"] opaque getNumHeartbeats : BaseIO Nat
/--
Adjusts the heartbeat counter of the current thread by the given amount. This can be useful to give
allocation-avoiding code additional "weight" and is also used to adjust the counter after resuming
from a snapshot.
-/
@[extern "lean_io_add_heartbeats"] opaque addHeartbeats (count : UInt64) : BaseIO Unit
/--
The mode of a file handle (i.e., a set of `open` flags and an `fdopen` mode).
@@ -786,6 +793,32 @@ instance : MonadLift (ST IO.RealWorld) BaseIO := ⟨id⟩
def mkRef (a : α) : BaseIO (IO.Ref α) :=
ST.mkRef a
/--
Mutable cell that can be passed around for purposes of cooperative task cancellation: request
cancellation with `CancelToken.set` and check for it with `CancelToken.isSet`.
This is a more flexible alternative to `Task.cancel` as the token can be shared between multiple
tasks.
-/
structure CancelToken where
private ref : IO.Ref Bool
namespace CancelToken
/-- Creates a new cancellation token. -/
def new : BaseIO CancelToken :=
CancelToken.mk <$> IO.mkRef false
/-- Activates a cancellation token. Idempotent. -/
def set (tk : CancelToken) : BaseIO Unit :=
tk.ref.set true
/-- Checks whether the cancellation token has been activated. -/
def isSet (tk : CancelToken) : BaseIO Bool :=
tk.ref.get
end CancelToken
namespace FS
namespace Stream

View File

@@ -835,7 +835,8 @@ syntax (name := renameI) "rename_i" (ppSpace colGt binderIdent)+ : tactic
/--
`repeat tac` repeatedly applies `tac` to the main goal until it fails.
That is, if `tac` produces multiple subgoals, only subgoals up to the first failure will be visited.
The `Batteries` library provides `repeat'` which repeats separately in each subgoal.
See also the tactic `repeat'` which repeats separately in each subgoal.
-/
syntax "repeat " tacticSeq : tactic
macro_rules

View File

@@ -67,13 +67,11 @@ def registerBuiltinAttribute (attr : AttributeImpl) : IO Unit := do
Helper methods for decoding the parameters of builtin attributes that are defined before `Lean.Parser`.
We have the following ones:
```
@[builtin_attr_parser] def simple := leading_parser ident >> optional ident >> optional priorityParser
/- We can't use `simple` for `class`, `instance`, `export` and `macro` because they are keywords. -/
@[builtin_attr_parser] def «class» := leading_parser "class"
@[builtin_attr_parser] def «instance» := leading_parser "instance" >> optional priorityParser
@[builtin_attr_parser] def simple := leading_parser ident >> optional (ppSpace >> (priorityParser <|> ident))
@[builtin_attr_parser] def «macro» := leading_parser "macro " >> ident
@[builtin_attr_parser] def «export» := leading_parser "export " >> ident
```
Note that we need the parsers for `class`, `instance`, and `macros` because they are keywords.
Note that we need the parsers for `class`, `instance`, `export` and `macros` because they are keywords.
-/
def Attribute.Builtin.ensureNoArgs (stx : Syntax) : AttrM Unit := do

View File

@@ -193,12 +193,13 @@ def foldCharOfNat (beforeErasure : Bool) (a : Expr) : Option Expr := do
else
return mkUInt32Lit 0
def foldToNat (_ : Bool) (a : Expr) : Option Expr := do
def foldToNat (size : Nat) (_ : Bool) (a : Expr) : Option Expr := do
let n getNumLit a
return mkRawNatLit n
return mkRawNatLit (n % size)
def uintFoldToNatFns : List (Name × UnFoldFn) :=
numScalarTypes.foldl (fun r info => (info.toNatFn, foldToNat) :: r) []
numScalarTypes.foldl (fun r info => (info.toNatFn, foldToNat info.size) :: r) []
def unFoldFns : List (Name × UnFoldFn) :=
[(``Nat.succ, foldNatSucc),

View File

@@ -5,6 +5,7 @@ Authors: Leonardo de Moura
-/
prelude
import Lean.AddDecl
import Lean.MonadEnv
import Lean.Elab.InfoTree.Main
namespace Lean
@@ -139,7 +140,7 @@ def setBuiltinInitAttr (env : Environment) (declName : Name) (initFnName : Name
builtinInitAttr.setParam env declName initFnName
def declareBuiltin (forDecl : Name) (value : Expr) : CoreM Unit := do
let name := `_regBuiltin ++ forDecl
let name mkAuxName (`_regBuiltin ++ forDecl) 1
let type := mkApp (mkConst `IO) (mkConst `Unit)
let decl := Declaration.defnDecl { name, levelParams := [], type, value, hints := ReducibilityHints.opaque,
safety := DefinitionSafety.safe }

View File

@@ -11,6 +11,7 @@ import Lean.Eval
import Lean.ResolveName
import Lean.Elab.InfoTree.Types
import Lean.MonadEnv
import Lean.Elab.Exception
namespace Lean
register_builtin_option diagnostics : Bool := {
@@ -85,6 +86,13 @@ structure Context where
Use the `set_option diag true` to set it to true.
-/
diag : Bool := false
/-- If set, used to cancel elaboration from outside when results are not needed anymore. -/
cancelTk? : Option IO.CancelToken := none
/--
If set (when `showPartialSyntaxErrors` is not set and parsing failed), suppresses most elaboration
errors; see also `logMessage` below.
-/
suppressElabErrors : Bool := false
deriving Nonempty
/-- CoreM is a monad for manipulating the Lean environment.
@@ -201,16 +209,45 @@ instance : MonadTrace CoreM where
getTraceState := return ( get).traceState
modifyTraceState f := modify fun s => { s with traceState := f s.traceState }
/-- Restore backtrackable parts of the state. -/
def restore (b : State) : CoreM Unit :=
modify fun s => { s with env := b.env, messages := b.messages, infoState := b.infoState }
structure SavedState extends State where
/-- Number of heartbeats passed inside `withRestoreOrSaveFull`, not used otherwise. -/
passedHearbeats : Nat
deriving Nonempty
def saveState : CoreM SavedState := do
let s get
return { toState := s, passedHearbeats := 0 }
/--
Restores full state including sources for unique identifiers. Only intended for incremental reuse
between elaboration runs, not for backtracking within a single run.
Incremental reuse primitive: if `reusableResult?` is `none`, runs `cont` with an action `save` that
on execution returns the saved monadic state at this point including the heartbeats used by `cont`
so far. If `reusableResult?` on the other hand is `some (a, state)`, restores full `state` including
heartbeats used and returns `a`.
The intention is for steps that support incremental reuse to initially pass `none` as
`reusableResult?` and call `save` as late as possible in `cont`. In a further run, if reuse is
possible, `reusableResult?` should be set to the previous state and result, ensuring that the state
after running `withRestoreOrSaveFull` is identical in both runs. Note however that necessarily this
is only an approximation in the case of heartbeats as heartbeats used by `withRestoreOrSaveFull`, by
the remainder of `cont` after calling `save`, as well as by reuse-handling code such as the one
supplying `reusableResult?` are not accounted for.
-/
def restoreFull (b : State) : CoreM Unit :=
set b
@[specialize] def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState))
(cont : (save : CoreM SavedState) CoreM α) : CoreM α := do
if let some (val, state) := reusableResult? then
set state.toState
IO.addHeartbeats state.passedHearbeats.toUInt64
return val
let startHeartbeats IO.getNumHeartbeats
cont (do
let s get
let stopHeartbeats IO.getNumHeartbeats
return { toState := s, passedHearbeats := stopHeartbeats - startHeartbeats })
/-- Restore backtrackable parts of the state. -/
def SavedState.restore (b : SavedState) : CoreM Unit :=
modify fun s => { s with env := b.env, messages := b.messages, infoState := b.infoState }
private def mkFreshNameImp (n : Name) : CoreM Name := do
let fresh modifyGet fun s => (s.nextMacroScope, { s with nextMacroScope := s.nextMacroScope + 1 })
@@ -241,10 +278,18 @@ instance [MetaEval α] : MetaEval (CoreM α) where
protected def withIncRecDepth [Monad m] [MonadControlT CoreM m] (x : m α) : m α :=
controlAt CoreM fun runInBase => withIncRecDepth (runInBase x)
builtin_initialize interruptExceptionId : InternalExceptionId registerInternalExceptionId `interrupt
/--
Throws an internal interrupt exception if cancellation has been requested. The exception is not
caught by `try catch` but is intended to be caught by `Command.withLoggingExceptions` at the top
level of elaboration. In particular, we want to skip producing further incremental snapshots after
the exception has been thrown.
-/
@[inline] def checkInterrupted : CoreM Unit := do
if ( IO.checkCanceled) then
-- should never be visible to users!
throw <| Exception.error .missing "elaboration interrupted"
if let some tk := ( read).cancelTk? then
if ( tk.isSet) then
throw <| .internal interruptExceptionId
register_builtin_option debug.moduleNameAtTimeout : Bool := {
defValue := true
@@ -289,11 +334,13 @@ def getMessageLog : CoreM MessageLog :=
return ( get).messages
/--
Returns the current log and then resets its messages but does NOT reset `MessageLog.hadErrors`. Used
Returns the current log and then resets its messages while adjusting `MessageLog.hadErrors`. Used
for incremental reporting during elaboration of a single command.
-/
def getAndEmptyMessageLog : CoreM MessageLog :=
modifyGet fun log => ({ log with msgs := {} }, log)
modifyGet fun s => (s.messages, { s with
messages.unreported := {}
messages.hadErrors := s.messages.hasErrors })
instance : MonadLog CoreM where
getRef := getRef
@@ -301,6 +348,12 @@ instance : MonadLog CoreM where
getFileName := return ( read).fileName
hasErrors := return ( get).messages.hasErrors
logMessage msg := do
if ( read).suppressElabErrors then
-- discard elaboration errors, except for a few important and unlikely misleading ones, on
-- parse error
unless msg.data.hasTag (· matches `Elab.synthPlaceholder | `Tactic.unsolvedGoals) do
return
let ctx read
let msg := { msg with data := MessageData.withNamingContext { currNamespace := ctx.currNamespace, openDecls := ctx.openDecls } msg.data };
modify fun s => { s with messages := s.messages.add msg }
@@ -408,19 +461,26 @@ def ImportM.runCoreM (x : CoreM α) : ImportM α := do
let (a, _) (withOptions (fun _ => ctx.opts) x).toIO { fileName := "<ImportM>", fileMap := default } { env := ctx.env }
return a
/-- Return `true` if the exception was generated by one our resource limits. -/
/-- Return `true` if the exception was generated by one of our resource limits. -/
def Exception.isRuntime (ex : Exception) : Bool :=
ex.isMaxHeartbeat || ex.isMaxRecDepth
/-- Returns `true` if the exception is an interrupt generated by `checkInterrupted`. -/
def Exception.isInterrupt : Exception Bool
| Exception.internal id _ => id == Core.interruptExceptionId
| _ => false
/--
Custom `try-catch` for all monads based on `CoreM`. We don't want to catch "runtime exceptions"
in these monads, but on `CommandElabM`. See issues #2775 and #2744 as well as `MonadAlwayExcept`.
Custom `try-catch` for all monads based on `CoreM`. We usually don't want to catch "runtime
exceptions" these monads, but on `CommandElabM`. See issues #2775 and #2744 as well as
`MonadAlwaysExcept`. Also, we never want to catch interrupt exceptions inside the elaborator.
-/
@[inline] protected def Core.tryCatch (x : CoreM α) (h : Exception CoreM α) : CoreM α := do
try
x
catch ex =>
if ex.isRuntime then
if ex.isInterrupt || ex.isRuntime then
throw ex -- We should use `tryCatchRuntimeEx` for catching runtime exceptions
else
h ex

View File

@@ -249,6 +249,8 @@ def toArray (m : HashMap α β) : Array (α × β) :=
def numBuckets (m : HashMap α β) : Nat :=
m.val.buckets.val.size
variable [BEq α] [Hashable α]
/-- Builds a `HashMap` from a list of key-value pairs. Values of duplicated keys are replaced by their respective last occurrences. -/
def ofList (l : List (α × β)) : HashMap α β :=
l.foldl (init := HashMap.empty) (fun m p => m.insert p.fst p.snd)
@@ -260,6 +262,7 @@ def ofListWith (l : List (α × β)) (f : β → β → β) : HashMap α β :=
match m.find? p.fst with
| none => m.insert p.fst p.snd
| some v => m.insert p.fst $ f v p.snd)
end Lean.HashMap
/--

View File

@@ -106,7 +106,7 @@ def ofPosition (text : FileMap) (pos : Position) : String.Pos :=
/--
Returns the position of the start of (1-based) line `line`.
This gives the stame result as `map.ofPosition ⟨line, 0⟩`, but is more efficient.
This gives the same result as `map.ofPosition ⟨line, 0⟩`, but is more efficient.
-/
def lineStart (map : FileMap) (line : Nat) : String.Pos :=
if h : line - 1 < map.positions.size then

View File

@@ -47,8 +47,9 @@ structure Context where
ref : Syntax := Syntax.missing
tacticCache? : Option (IO.Ref Tactic.Cache)
/--
Snapshot for incremental reuse and reporting of command elaboration. Currently unused in Lean
itself.
Snapshot for incremental reuse and reporting of command elaboration. Currently only used for
(mutual) defs and contained tactics, in which case the `DynamicSnapshot` is a
`HeadersParsedSnapshot`.
Definitely resolved in `Language.Lean.process.doElab`.
@@ -56,6 +57,13 @@ structure Context where
old elaboration are identical.
-/
snap? : Option (Language.SnapshotBundle Language.DynamicSnapshot)
/-- Cancellation token forwarded to `Core.cancelTk?`. -/
cancelTk? : Option IO.CancelToken
/--
If set (when `showPartialSyntaxErrors` is not set and parsing failed), suppresses most elaboration
errors; see also `logMessage` below.
-/
suppressElabErrors : Bool := false
abbrev CommandElabCoreM (ε) := ReaderT Context $ StateRefT State $ EIO ε
abbrev CommandElabM := CommandElabCoreM Exception
@@ -73,6 +81,21 @@ Remark: see comment at TermElabM
@[always_inline]
instance : Monad CommandElabM := let i := inferInstanceAs (Monad CommandElabM); { pure := i.pure, bind := i.bind }
/-- Like `Core.tryCatch` but do catch runtime exceptions. -/
@[inline] protected def tryCatch (x : CommandElabM α) (h : Exception CommandElabM α) :
CommandElabM α := do
try
x
catch ex =>
if ex.isInterrupt then
throw ex
else
h ex
instance : MonadExceptOf Exception CommandElabM where
throw := throw
tryCatch := Command.tryCatch
def mkState (env : Environment) (messages : MessageLog := {}) (opts : Options := {}) : State := {
env := env
messages := messages
@@ -160,17 +183,18 @@ private def runCore (x : CoreM α) : CommandElabM α := do
let env := Kernel.resetDiag s.env
let scope := s.scopes.head!
let coreCtx : Core.Context := {
fileName := ctx.fileName
fileMap := ctx.fileMap
currRecDepth := ctx.currRecDepth
maxRecDepth := s.maxRecDepth
ref := ctx.ref
currNamespace := scope.currNamespace
openDecls := scope.openDecls
initHeartbeats := heartbeats
currMacroScope := ctx.currMacroScope
options := scope.opts
}
fileName := ctx.fileName
fileMap := ctx.fileMap
currRecDepth := ctx.currRecDepth
maxRecDepth := s.maxRecDepth
ref := ctx.ref
currNamespace := scope.currNamespace
openDecls := scope.openDecls
initHeartbeats := heartbeats
currMacroScope := ctx.currMacroScope
options := scope.opts
cancelTk? := ctx.cancelTk?
suppressElabErrors := ctx.suppressElabErrors }
let x : EIO _ _ := x.run coreCtx {
env
ngen := s.ngen
@@ -215,6 +239,11 @@ instance : MonadLog CommandElabM where
getFileName := return ( read).fileName
hasErrors := return ( get).messages.hasErrors
logMessage msg := do
if ( read).suppressElabErrors then
-- discard elaboration errors on parse error
-- NOTE: unlike `CoreM`'s `logMessage`, we do not currently have any command-level errors that
-- we want to allowlist
return
let currNamespace getCurrNamespace
let openDecls getOpenDecls
let msg := { msg with data := MessageData.withNamingContext { currNamespace := currNamespace, openDecls := openDecls } msg.data }
@@ -267,11 +296,29 @@ private def mkInfoTree (elaborator : Name) (stx : Syntax) (trees : PersistentArr
}
return InfoTree.context ctx tree
/--
Disables incremental command reuse *and* reporting for `act` if `cond` is true by setting
`Context.snap?` to `none`.
-/
def withoutCommandIncrementality (cond : Bool) (act : CommandElabM α) : CommandElabM α := do
let opts getOptions
withReader (fun ctx => { ctx with snap? := ctx.snap?.filter fun snap => Id.run do
if let some old := snap.old? then
if cond && opts.getBool `trace.Elab.reuse then
dbg_trace "reuse stopped: guard failed at {old.stx}"
return !cond
}) act
private def elabCommandUsing (s : State) (stx : Syntax) : List (KeyedDeclsAttribute.AttributeEntry CommandElab) CommandElabM Unit
| [] => withInfoTreeContext (mkInfoTree := mkInfoTree `no_elab stx) <| throwError "unexpected syntax{indentD stx}"
| (elabFn::elabFns) =>
catchInternalId unsupportedSyntaxExceptionId
(withInfoTreeContext (mkInfoTree := mkInfoTree elabFn.declName stx) <| elabFn.value stx)
(do
-- prevent unsupported commands from accidentally accessing `Context.snap?` (e.g. by nested
-- supported commands)
withoutCommandIncrementality (!( isIncrementalElab elabFn.declName)) do
withInfoTreeContext (mkInfoTree := mkInfoTree elabFn.declName stx) do
elabFn.value stx)
(fun _ => do set s; elabCommandUsing s stx elabFns)
/-- Elaborate `x` with `stx` on the macro stack -/
@@ -298,7 +345,10 @@ partial def elabCommand (stx : Syntax) : CommandElabM Unit := do
if k == nullKind then
-- list of commands => elaborate in order
-- The parser will only ever return a single command at a time, but syntax quotations can return multiple ones
args.forM elabCommand
-- TODO: support incrementality at least for some cases such as expansions of
-- `set_option in` or `def a.b`
withoutCommandIncrementality true do
args.forM elabCommand
else withTraceNode `Elab.command (fun _ => return stx) (tag :=
-- special case: show actual declaration kind for `declaration` commands
(if stx.isOfKind ``Parser.Command.declaration then stx[1] else stx).getKind.toString) do
@@ -321,11 +371,19 @@ partial def elabCommand (stx : Syntax) : CommandElabM Unit := do
builtin_initialize registerTraceClass `Elab.input
/-- Option for showing elaboration errors from partial syntax errors. -/
register_builtin_option showPartialSyntaxErrors : Bool := {
defValue := false
descr := "show elaboration errors from partial syntax trees (i.e. after parser recovery)"
}
/--
`elabCommand` wrapper that should be used for the initial invocation, not for recursive calls after
macro expansion etc.
-/
def elabCommandTopLevel (stx : Syntax) : CommandElabM Unit := withRef stx do profileitM Exception "elaboration" ( getOptions) do
withReader ({ · with suppressElabErrors :=
stx.hasMissing && !showPartialSyntaxErrors.get ( getOptions) }) do
let initMsgs modifyGet fun st => (st.messages, { st with messages := {} })
let initInfoTrees getResetInfoTrees
try
@@ -462,7 +520,12 @@ def runTermElabM (elabFn : Array Expr → TermElabM α) : CommandElabM α := do
Term.addAutoBoundImplicits' xs someType fun xs _ =>
Term.withoutAutoBoundImplicit <| elabFn xs
@[inline] def catchExceptions (x : CommandElabM Unit) : CommandElabCoreM Empty Unit := fun ctx ref =>
/--
Catches and logs exceptions occurring in `x`. Unlike `try catch` in `CommandElabM`, this function
catches interrupt exceptions as well and thus is intended for use at the top level of elaboration.
Interrupt and abort exceptions are caught but not logged.
-/
@[inline] def withLoggingExceptions (x : CommandElabM Unit) : CommandElabCoreM Empty Unit := fun ctx ref =>
EIO.catchExceptions (withLogging x ctx ref) (fun _ => pure ())
private def liftAttrM {α} (x : AttrM α) : CommandElabM α := do
@@ -528,6 +591,7 @@ def liftCommandElabM (cmd : CommandElabM α) : CoreM α := do
ref := getRef
tacticCache? := none
snap? := none
cancelTk? := ( read).cancelTk?
} |>.run {
env := getEnv
maxRecDepth := getMaxRecDepth
@@ -537,7 +601,7 @@ def liftCommandElabM (cmd : CommandElabM α) : CoreM α := do
traceState.traces := coreState.traceState.traces ++ commandState.traceState.traces
env := commandState.env
}
if let some err := commandState.messages.msgs.toArray.find? (·.severity matches .error) then
if let some err := commandState.messages.toArray.find? (·.severity matches .error) then
throwError err.data
pure a

View File

@@ -188,7 +188,7 @@ def elabClassInductive (modifiers : Modifiers) (stx : Syntax) : CommandElabM Uni
let v classInductiveSyntaxToView modifiers stx
elabInductiveViews #[v]
@[builtin_command_elab declaration]
@[builtin_command_elab declaration, builtin_incremental]
def elabDeclaration : CommandElab := fun stx => do
match ( liftMacroM <| expandDeclNamespace? stx) with
| some (ns, newStx) => do
@@ -198,22 +198,24 @@ def elabDeclaration : CommandElab := fun stx => do
| none => do
let decl := stx[1]
let declKind := decl.getKind
if declKind == ``Lean.Parser.Command.«axiom» then
let modifiers elabModifiers stx[0]
elabAxiom modifiers decl
else if declKind == ``Lean.Parser.Command.«inductive» then
let modifiers elabModifiers stx[0]
elabInductive modifiers decl
else if declKind == ``Lean.Parser.Command.classInductive then
let modifiers elabModifiers stx[0]
elabClassInductive modifiers decl
else if declKind == ``Lean.Parser.Command.«structure» then
let modifiers elabModifiers stx[0]
elabStructure modifiers decl
else if isDefLike decl then
if isDefLike decl then
-- only case implementing incrementality currently
elabMutualDef #[stx]
else
throwError "unexpected declaration"
else withoutCommandIncrementality true do
if declKind == ``Lean.Parser.Command.«axiom» then
let modifiers elabModifiers stx[0]
elabAxiom modifiers decl
else if declKind == ``Lean.Parser.Command.«inductive» then
let modifiers elabModifiers stx[0]
elabInductive modifiers decl
else if declKind == ``Lean.Parser.Command.classInductive then
let modifiers elabModifiers stx[0]
elabClassInductive modifiers decl
else if declKind == ``Lean.Parser.Command.«structure» then
let modifiers elabModifiers stx[0]
elabStructure modifiers decl
else
throwError "unexpected declaration"
/-- Return true if all elements of the mutual-block are inductive declarations. -/
private def isMutualInductive (stx : Syntax) : Bool :=
@@ -322,14 +324,16 @@ def expandMutualPreamble : Macro := fun stx =>
let endCmd `(end)
return mkNullNode (#[secCmd] ++ preamble ++ #[newMutual] ++ #[endCmd])
@[builtin_command_elab «mutual»]
@[builtin_command_elab «mutual», builtin_incremental]
def elabMutual : CommandElab := fun stx => do
if isMutualInductive stx then
elabMutualInductive stx[1].getArgs
else if isMutualDef stx then
if isMutualDef stx then
-- only case implementing incrementality currently
elabMutualDef stx[1].getArgs
else
throwError "invalid mutual block: either all elements of the block must be inductive declarations, or they must all be definitions/theorems/abbrevs"
else withoutCommandIncrementality true do
if isMutualInductive stx then
elabMutualInductive stx[1].getArgs
else
throwError "invalid mutual block: either all elements of the block must be inductive declarations, or they must all be definitions/theorems/abbrevs"
/- leading_parser "attribute " >> "[" >> sepBy1 (eraseAttr <|> Term.attrInstance) ", " >> "]" >> many1 ident -/
@[builtin_command_elab «attribute»] def elabAttr : CommandElab := fun stx => do

View File

@@ -28,14 +28,101 @@ def DefKind.isExample : DefKind → Bool
| .example => true
| _ => false
/-- Header elaboration data of a `DefView`. -/
structure DefViewElabHeaderData where
/--
Short name. Recall that all declarations in Lean 4 are potentially recursive. We use `shortDeclName` to refer
to them at `valueStx`, and other declarations in the same mutual block. -/
shortDeclName : Name
/-- Full name for this declaration. This is the name that will be added to the `Environment`. -/
declName : Name
/-- Universe level parameter names explicitly provided by the user. -/
levelNames : List Name
/-- Syntax objects for the binders occurring before `:`, we use them to populate the `InfoTree` when elaborating `valueStx`. -/
binderIds : Array Syntax
/-- Number of parameters before `:`, it also includes auto-implicit parameters automatically added by Lean. -/
numParams : Nat
/-- Type including parameters. -/
type : Expr
deriving Inhabited
section Snapshots
open Language
/-- Snapshot after processing of a definition body. -/
structure BodyProcessedSnapshot extends Language.Snapshot where
/-- State after elaboration. -/
state : Term.SavedState
/-- Elaboration result. -/
value : Expr
deriving Nonempty
instance : Language.ToSnapshotTree BodyProcessedSnapshot where
toSnapshotTree s := s.toSnapshot, #[]
/-- Snapshot after elaboration of a definition header. -/
structure HeaderProcessedSnapshot extends Language.Snapshot where
/-- Elaboration results. -/
view : DefViewElabHeaderData
/-- Resulting elaboration state, including any environment additions. -/
state : Term.SavedState
/-- Syntax of top-level tactic block if any, for checking reuse of `tacSnap?`. -/
tacStx? : Option Syntax
/-- Incremental execution of main tactic block, if any. -/
tacSnap? : Option (SnapshotTask Tactic.TacticParsedSnapshot)
/-- Syntax of definition body, for checking reuse of `bodySnap`. -/
bodyStx : Syntax
/-- Result of body elaboration. -/
bodySnap : SnapshotTask (Option BodyProcessedSnapshot)
deriving Nonempty
instance : Language.ToSnapshotTree HeaderProcessedSnapshot where
toSnapshotTree s := s.toSnapshot,
(match s.tacSnap? with
| some tac => #[tac.map (sync := true) toSnapshotTree]
| none => #[]) ++
#[s.bodySnap.map (sync := true) toSnapshotTree]
/-- State before elaboration of a mutual definition. -/
structure DefParsed where
/--
Unstructured syntax object comprising the full "header" of the definition from the modifiers
(incl. docstring) up to the value, used for determining header elaboration reuse.
-/
fullHeaderRef : Syntax
/-- Elaboration result, unless fatal exception occurred. -/
headerProcessedSnap : SnapshotTask (Option HeaderProcessedSnapshot)
deriving Nonempty
/-- Snapshot after syntax tree has been split into separate mutual def headers. -/
structure DefsParsedSnapshot extends Language.Snapshot where
/-- Definitions of this mutual block. -/
defs : Array DefParsed
deriving Nonempty, TypeName
instance : Language.ToSnapshotTree DefsParsedSnapshot where
toSnapshotTree s := s.toSnapshot,
s.defs.map (·.headerProcessedSnap.map (sync := true) toSnapshotTree)
end Snapshots
structure DefView where
kind : DefKind
ref : Syntax
/--
An unstructured syntax object that comprises the "header" of the definition, i.e. everything up
to the value. Used as a more specific ref for header elaboration.
-/
headerRef : Syntax
modifiers : Modifiers
declId : Syntax
binders : Syntax
type? : Option Syntax
value : Syntax
/--
Snapshot for incremental processing of this definition.
Invariant: If the bundle's `old?` is set, then elaboration of the header is guaranteed to result
in the same elaboration result and state, i.e. reuse is possible.
-/
headerSnap? : Option (Language.SnapshotBundle (Option HeaderProcessedSnapshot)) := none
deriving? : Option (Array Syntax) := none
deriving Inhabited
@@ -50,20 +137,20 @@ def mkDefViewOfAbbrev (modifiers : Modifiers) (stx : Syntax) : DefView :=
let (binders, type) := expandOptDeclSig stx[2]
let modifiers := modifiers.addAttribute { name := `inline }
let modifiers := modifiers.addAttribute { name := `reducible }
{ ref := stx, kind := DefKind.abbrev, modifiers,
{ ref := stx, headerRef := mkNullNode stx.getArgs[:3], kind := DefKind.abbrev, modifiers,
declId := stx[1], binders, type? := type, value := stx[3] }
def mkDefViewOfDef (modifiers : Modifiers) (stx : Syntax) : DefView :=
-- leading_parser "def " >> declId >> optDeclSig >> declVal >> optDefDeriving
let (binders, type) := expandOptDeclSig stx[2]
let deriving? := if stx[4].isNone then none else some stx[4][1].getSepArgs
{ ref := stx, kind := DefKind.def, modifiers,
{ ref := stx, headerRef := mkNullNode stx.getArgs[:3], kind := DefKind.def, modifiers,
declId := stx[1], binders, type? := type, value := stx[3], deriving? }
def mkDefViewOfTheorem (modifiers : Modifiers) (stx : Syntax) : DefView :=
-- leading_parser "theorem " >> declId >> declSig >> declVal
let (binders, type) := expandDeclSig stx[2]
{ ref := stx, kind := DefKind.theorem, modifiers,
{ ref := stx, headerRef := mkNullNode stx.getArgs[:3], kind := DefKind.theorem, modifiers,
declId := stx[1], binders, type? := some type, value := stx[3] }
def mkDefViewOfInstance (modifiers : Modifiers) (stx : Syntax) : CommandElabM DefView := do
@@ -84,7 +171,7 @@ def mkDefViewOfInstance (modifiers : Modifiers) (stx : Syntax) : CommandElabM De
trace[Elab.instance.mkInstanceName] "generated {(← getCurrNamespace) ++ id}"
pure <| mkNode ``Parser.Command.declId #[mkIdentFrom stx id, mkNullNode]
return {
ref := stx, kind := DefKind.def, modifiers := modifiers,
ref := stx, headerRef := mkNullNode stx.getArgs[:5], kind := DefKind.def, modifiers := modifiers,
declId := declId, binders := binders, type? := type, value := stx[5]
}
@@ -97,7 +184,7 @@ def mkDefViewOfOpaque (modifiers : Modifiers) (stx : Syntax) : CommandElabM DefV
let val if modifiers.isUnsafe then `(default_or_ofNonempty% unsafe) else `(default_or_ofNonempty%)
`(Parser.Command.declValSimple| := $val)
return {
ref := stx, kind := DefKind.opaque, modifiers := modifiers,
ref := stx, headerRef := mkNullNode stx.getArgs[:3], kind := DefKind.opaque, modifiers := modifiers,
declId := stx[1], binders := binders, type? := some type, value := val
}
@@ -106,7 +193,7 @@ def mkDefViewOfExample (modifiers : Modifiers) (stx : Syntax) : DefView :=
let (binders, type) := expandOptDeclSig stx[1]
let id := mkIdentFrom stx `_example
let declId := mkNode ``Parser.Command.declId #[id, mkNullNode]
{ ref := stx, kind := DefKind.example, modifiers := modifiers,
{ ref := stx, headerRef := mkNullNode stx.getArgs[:2], kind := DefKind.example, modifiers := modifiers,
declId := declId, binders := binders, type? := type, value := stx[2] }
def isDefLike (stx : Syntax) : Bool :=

View File

@@ -182,7 +182,8 @@ def mkDecEqEnum (declName : Name) : CommandElabM Unit := do
fun x y =>
if h : x.toCtorIdx = y.toCtorIdx then
-- We use `rfl` in the following proof because the first script fails for unit-like datatypes due to etaStruct.
isTrue (by first | have aux := congrArg $ofNatIdent h; rw [$auxThmIdent:ident, $auxThmIdent:ident] at aux; assumption | rfl)
-- Temporarily avoiding tactic `have` for bootstrapping
isTrue (by first | refine_lift have aux := congrArg $ofNatIdent h; ?_; rw [$auxThmIdent:ident, $auxThmIdent:ident] at aux; assumption | rfl)
else
isFalse fun h => by subst h; contradiction
)

View File

@@ -688,27 +688,15 @@ def getDoLetVars (doLet : Syntax) : TermElabM (Array Var) :=
-- leading_parser "let " >> optional "mut " >> letDecl
getLetDeclVars doLet[2]
def getHaveIdLhsVar (optIdent : Syntax) : Var :=
if optIdent.getKind == hygieneInfoKind then
HygieneInfo.mkIdent optIdent[0] `this
else
optIdent
def getDoHaveVars (doHave : Syntax) : TermElabM (Array Var) := do
-- doHave := leading_parser "have " >> Term.haveDecl
-- haveDecl := leading_parser haveIdDecl <|> letPatDecl <|> haveEqnsDecl
let arg := doHave[1][0]
if arg.getKind == ``Parser.Term.haveIdDecl then
-- haveIdDecl := leading_parser atomic (haveIdLhs >> " := ") >> termParser
-- haveIdLhs := (binderIdent <|> hygieneInfo) >> many letIdBinder >> optType
return #[getHaveIdLhsVar arg[0]]
else if arg.getKind == ``Parser.Term.letPatDecl then
getLetPatDeclVars arg
else if arg.getKind == ``Parser.Term.haveEqnsDecl then
-- haveEqnsDecl := leading_parser haveIdLhs >> matchAlts
return #[getHaveIdLhsVar arg[0]]
else
throwError "unexpected kind of have declaration"
def getDoHaveVars : Syntax TermElabM (Array Var)
-- NOTE: `hygieneInfo` case should come first as `id` will match anything else
| `(doElem| have $info:hygieneInfo $_params* $[$_:typeSpec]? := $_val)
| `(doElem| have $info:hygieneInfo $_params* $[$_:typeSpec]? $_eqns:matchAlts) =>
return #[HygieneInfo.mkIdent info `this]
| `(doElem| have $id $_params* $[$_:typeSpec]? := $_val)
| `(doElem| have $id $_params* $[$_:typeSpec]? $_eqns:matchAlts) => return #[id]
| `(doElem| have $pat:letPatDecl) => getLetPatDeclVars pat
| _ => throwError "unexpected kind of have declaration"
def getDoLetRecVars (doLetRec : Syntax) : TermElabM (Array Var) := do
-- letRecDecls is an array of `(group (optional attributes >> letDecl))`

View File

@@ -5,7 +5,7 @@ Authors: Leonardo de Moura
-/
prelude
import Lean.InternalExceptionId
import Lean.Meta.Basic
import Lean.Exception
namespace Lean.Elab

View File

@@ -16,6 +16,7 @@ structure State where
parserState : Parser.ModuleParserState
cmdPos : String.Pos
commands : Array Syntax := #[]
deriving Nonempty
structure Context where
inputCtx : Parser.InputContext
@@ -34,6 +35,7 @@ def setCommandState (commandState : Command.State) : FrontendM Unit :=
fileMap := ctx.inputCtx.fileMap
tacticCache? := none
snap? := none
cancelTk? := none
}
match ( liftM <| EIO.toIO' <| (x cmdCtx).run s.commandState) with
| Except.error e => throw <| IO.Error.userError s!"unexpected internal error: {← e.toMessageData.toString}"
@@ -44,15 +46,6 @@ def elabCommandAtFrontend (stx : Syntax) : FrontendM Unit := do
let initMsgs modifyGet fun st => (st.messages, { st with messages := {} })
Command.elabCommandTopLevel stx
let mut msgs := ( get).messages
-- `stx.hasMissing` should imply `initMsgs.hasErrors`, but the latter should be cheaper to check
-- in general
if !Language.Lean.showPartialSyntaxErrors.get ( getOptions) && initMsgs.hasErrors &&
stx.hasMissing then
-- discard elaboration errors, except for a few important and unlikely misleading ones, on
-- parse error
msgs := msgs.msgs.filter fun msg =>
msg.data.hasTag (fun tag => tag == `Elab.synthPlaceholder ||
tag == `Tactic.unsolvedGoals || (`_traceMsg).isSuffixOf tag)
modify ({ · with messages := initMsgs ++ msgs })
def updateCmdPos : FrontendM Unit := do
@@ -92,6 +85,47 @@ def IO.processCommands (inputCtx : Parser.InputContext) (parserState : Parser.Mo
let (_, s) (Frontend.processCommands.run { inputCtx := inputCtx }).run { commandState := commandState, parserState := parserState, cmdPos := parserState.pos }
pure s
structure IncrementalState extends State where
inputCtx : Parser.InputContext
initialSnap : Language.Lean.CommandParsedSnapshot
deriving Nonempty
open Language in
/--
Variant of `IO.processCommands` that uses the new Lean language processor implementation for
potential incremental reuse. Pass in result of a previous invocation done with the same state
(but usually different input context) to allow for reuse.
-/
-- `IO.processCommands` can be reimplemented on top of this as soon as the additional tasks speed up
-- things instead of slowing them down
partial def IO.processCommandsIncrementally (inputCtx : Parser.InputContext)
(parserState : Parser.ModuleParserState) (commandState : Command.State)
(old? : Option IncrementalState) :
BaseIO IncrementalState := do
let task Language.Lean.processCommands inputCtx parserState commandState
(old?.map fun old => (old.inputCtx, old.initialSnap))
go task.get task #[]
where
go initialSnap t commands :=
let snap := t.get
let commands := commands.push snap.data.stx
if let some next := snap.nextCmdSnap? then
go initialSnap next commands
else
-- Opting into reuse also enables incremental reporting, so make sure to collect messages from
-- all snapshots
let messages := toSnapshotTree initialSnap
|>.getAll.map (·.diagnostics.msgLog)
|>.foldl (· ++ ·) {}
let trees := toSnapshotTree initialSnap
|>.getAll.map (·.infoTree?) |>.filterMap id |>.toPArray'
return {
commandState := { snap.data.finishedSnap.get.cmdState with messages, infoState.trees := trees }
parserState := snap.data.parserState
cmdPos := snap.data.parserState.pos
inputCtx, initialSnap, commands
}
def process (input : String) (env : Environment) (opts : Options) (fileName : Option String := none) : IO (Environment × MessageLog) := do
let fileName := fileName.getD "<input>"
let inputCtx := Parser.mkInputContext input fileName
@@ -113,8 +147,7 @@ def runFrontend
: IO (Environment × Bool) := do
let startTime := ( IO.monoNanosNow).toFloat / 1000000000
let inputCtx := Parser.mkInputContext input fileName
-- TODO: replace with `#lang` processing
if /- Lean #lang? -/ true then
if true then
-- Temporarily keep alive old cmdline driver for the Lean language so that we don't pay the
-- overhead of passing the environment between snapshots until we actually make good use of it
-- outside the server

View File

@@ -56,13 +56,11 @@ where
return Syntax.mkAntiquotNode kind term
| some (.category cat) =>
return Syntax.mkAntiquotNode cat term (isPseudoKind := true)
| none =>
| some (.alias _) =>
let id := id.getId.eraseMacroScopes
if ( Parser.isParserAlias id) then
let kind := ( Parser.getSyntaxKindOfParserAlias? id).getD Name.anonymous
return Syntax.mkAntiquotNode kind term
else
throwError "unknown parser declaration/category/alias '{id}'"
let kind := ( Parser.getSyntaxKindOfParserAlias? id).getD Name.anonymous
return Syntax.mkAntiquotNode kind term
| _ => throwError "unknown parser declaration/category/alias '{id}'"
| stx, term => do
-- can't match against `` `(stx| ($stxs*)) `` as `*` is interpreted as the `stx` operator
if stx.raw.isOfKind ``Parser.Syntax.paren then

View File

@@ -20,28 +20,24 @@ import Lean.Elab.DeclarationRange
namespace Lean.Elab
open Lean.Parser.Term
/-- `DefView` after elaborating the header. -/
structure DefViewElabHeader where
ref : Syntax
modifiers : Modifiers
/-- Stores whether this is the header of a definition, theorem, ... -/
kind : DefKind
open Language
/-- `DefView` plus header elaboration data and snapshot. -/
structure DefViewElabHeader extends DefView, DefViewElabHeaderData where
/--
Short name. Recall that all declarations in Lean 4 are potentially recursive. We use `shortDeclName` to refer
to them at `valueStx`, and other declarations in the same mutual block. -/
shortDeclName : Name
/-- Full name for this declaration. This is the name that will be added to the `Environment`. -/
declName : Name
/-- Universe level parameter names explicitly provided by the user. -/
levelNames : List Name
/-- Syntax objects for the binders occurring before `:`, we use them to populate the `InfoTree` when elaborating `valueStx`. -/
binderIds : Array Syntax
/-- Number of parameters before `:`, it also includes auto-implicit parameters automatically added by Lean. -/
numParams : Nat
/-- Type including parameters. -/
type : Expr
/-- `Syntax` object the body/value of the definition. -/
valueStx : Syntax
Snapshot for incremental processing of top-level tactic block, if any.
Invariant: if the bundle's `old?` is set, then the state *up to the start* of the tactic block is
unchanged, i.e. reuse is possible.
-/
tacSnap? : Option (Language.SnapshotBundle Tactic.TacticParsedSnapshot)
/--
Snapshot for incremental processing of definition body.
Invariant: if the bundle's `old?` is set, then elaboration of the body is guaranteed to result in
the same elaboration result and state, i.e. reuse is possible.
-/
bodySnap? : Option (Language.SnapshotBundle (Option BodyProcessedSnapshot))
deriving Inhabited
namespace Term
@@ -127,16 +123,71 @@ private def cleanupOfNat (type : Expr) : MetaM Expr := do
let eNew := mkApp e.appFn! argArgs[1]!
return .done eNew
/-- Elaborate only the declaration headers. We have to elaborate the headers first because we support mutually recursive declarations in Lean 4. -/
private def elabHeaders (views : Array DefView) : TermElabM (Array DefViewElabHeader) := do
let expandedDeclIds views.mapM fun view => withRef view.ref do
/--
Elaborates only the declaration view headers. We have to elaborate the headers first because we
support mutually recursive declarations in Lean 4.
-/
private def elabHeaders (views : Array DefView)
(bodyPromises : Array (IO.Promise (Option BodyProcessedSnapshot)))
(tacPromises : Array (IO.Promise Tactic.TacticParsedSnapshot)) :
TermElabM (Array DefViewElabHeader) := do
let expandedDeclIds views.mapM fun view => withRef view.headerRef do
Term.expandDeclId ( getCurrNamespace) ( getLevelNames) view.declId view.modifiers
withAutoBoundImplicitForbiddenPred (fun n => expandedDeclIds.any (·.shortName == n)) do
let mut headers := #[]
for view in views, shortDeclName, declName, levelNames in expandedDeclIds do
let newHeader withRef view.ref do
addDeclarationRanges declName view.ref
-- Can we reuse the result for a body? For starters, all headers (even those below the body)
-- must be reusable
let mut reuseBody := views.all (·.headerSnap?.any (·.old?.isSome))
for view in views, shortDeclName, declName, levelNames in expandedDeclIds,
tacPromise in tacPromises, bodyPromise in bodyPromises do
let mut reusableResult? := none
if let some snap := view.headerSnap? then
-- by the `DefView.headerSnap?` invariant, safe to reuse results at this point, so let's
-- wait for them!
if let some old := snap.old?.bind (·.val.get) then
let (tacStx?, newTacTask?) mkTacTask view.value tacPromise
snap.new.resolve <| some { old with
tacStx?
tacSnap? := newTacTask?
bodyStx := view.value
bodySnap := mkBodyTask view.value bodyPromise
}
-- Transition from `DefView.snap?` to `DefViewElabHeader.tacSnap?` invariant: if all
-- headers and all previous bodies could be reused, then the state at the *start* of the
-- top-level tactic block (if any) is unchanged
let reuseTac := reuseBody
-- Transition from `DefView.snap?` to `DefViewElabHeader.bodySnap?` invariant: if all
-- headers and all previous bodies could be reused and this body syntax is unchanged, then
-- we can reuse the result
reuseBody := reuseBody &&
view.value.structRangeEqWithTraceReuse ( getOptions) old.bodyStx
let header := { old.view, view with
-- We should only forward the promise if we are actually waiting on the corresponding
-- task; otherwise, diagnostics assigned to it will be lost
tacSnap? := guard newTacTask?.isSome *> some {
old? := do
guard reuseTac
some ( old.tacStx?), ( old.tacSnap?)
new := tacPromise
}
bodySnap? := some {
-- no syntax guard to store, we already did the necessary checks
old? := guard reuseBody *> pure .missing, old.bodySnap
new := bodyPromise
}
}
reusableResult? := some (header, old.state)
else
reuseBody := false
let header withRestoreOrSaveFull reusableResult? fun save => do
withRef view.headerRef do
addDeclarationRanges declName view.ref -- NOTE: this should be the full `ref`
applyAttributesAt declName view.modifiers.attrs .beforeElaboration
-- do not hide header errors on partial body syntax as these two elaboration parts are
-- sufficiently independent
withTheReader Core.Context ({ · with suppressElabErrors :=
view.headerRef.hasMissing && !Command.showPartialSyntaxErrors.get ( getOptions) }) do
withDeclName declName <| withAutoBoundImplicit <| withLevelNames levelNames <|
elabBindersEx view.binders.getArgs fun xs => do
let refForElabFunType := view.value
@@ -164,21 +215,62 @@ private def elabHeaders (views : Array DefView) : TermElabM (Array DefViewElabHe
let pendingMVarIds getMVars type
discard <| logUnassignedUsingErrorInfos pendingMVarIds <|
getPendindMVarErrorMessage views
let newHeader := {
ref := view.ref
modifiers := view.modifiers
kind := view.kind
shortDeclName := shortDeclName
declName, type, levelNames, binderIds
numParams := xs.size
valueStx := view.value : DefViewElabHeader }
let newHeader : DefViewElabHeaderData := {
declName, shortDeclName, type, levelNames, binderIds
numParams := xs.size
}
let mut newHeader : DefViewElabHeader := { view, newHeader with
bodySnap? := none, tacSnap? := none }
if let some snap := view.headerSnap? then
let (tacStx?, newTacTask?) mkTacTask view.value tacPromise
snap.new.resolve <| some {
diagnostics :=
( Language.Snapshot.Diagnostics.ofMessageLog ( Core.getAndEmptyMessageLog))
view := newHeader.toDefViewElabHeaderData
state := ( save)
tacStx?
tacSnap? := newTacTask?
bodyStx := view.value
bodySnap := mkBodyTask view.value bodyPromise
}
newHeader := { newHeader with
-- We should only forward the promise if we are actually waiting on the
-- corresponding task; otherwise, diagnostics assigned to it will be lost
tacSnap? := guard newTacTask?.isSome *> some { old? := none, new := tacPromise }
bodySnap? := some { old? := none, new := bodyPromise }
}
check headers newHeader
return newHeader
headers := headers.push newHeader
headers := headers.push header
return headers
where
getBodyTerm? (stx : Syntax) : Option Syntax :=
-- TODO: does not work with partial syntax
--| `(Parser.Command.declVal| := $body $_suffix:suffix $[$_where]?) => body
guard (stx.isOfKind ``Parser.Command.declValSimple) *> some stx[1]
/-- Creates snapshot task with appropriate range from body syntax and promise. -/
mkBodyTask (body : Syntax) (new : IO.Promise (Option BodyProcessedSnapshot)) :
Language.SnapshotTask (Option BodyProcessedSnapshot) :=
let rangeStx := getBodyTerm? body |>.getD body
{ range? := rangeStx.getRange?, task := new.result }
/--
If `body` allows for incremental tactic reporting and reuse, creates a snapshot task out of the
passed promise with appropriate range, otherwise immediately resolves the promise to a dummy
value.
-/
mkTacTask (body : Syntax) (tacPromise : IO.Promise Tactic.TacticParsedSnapshot) :
TermElabM (Option Syntax × Option (Language.SnapshotTask Tactic.TacticParsedSnapshot))
:= do
if let some e := getBodyTerm? body then
if let `(by $tacs*) := e then
return (e, some { range? := mkNullNode tacs |>.getRange?, task := tacPromise.result })
tacPromise.resolve default
return (none, none)
/--
Create auxiliary local declarations `fs` for the given hearders using their `shortDeclName` and `type`, given hearders, and execute `k fs`.
Create auxiliary local declarations `fs` for the given headers using their `shortDeclName` and `type`, given headers, and execute `k fs`.
The new free variables are tagged as `auxDecl`.
Remark: `fs.size = headers.size`.
-/
@@ -250,15 +342,44 @@ private def declValToTerminationHint (declVal : Syntax) : TermElabM WF.Terminati
return .none
private def elabFunValues (headers : Array DefViewElabHeader) : TermElabM (Array Expr) :=
headers.mapM fun header => withDeclName header.declName <| withLevelNames header.levelNames do
let valStx liftMacroM <| declValToTerm header.valueStx
forallBoundedTelescope header.type header.numParams fun xs type => do
-- Add new info nodes for new fvars. The server will detect all fvars of a binder by the binder's source location.
for i in [0:header.binderIds.size] do
-- skip auto-bound prefix in `xs`
addLocalVarInfo header.binderIds[i]! xs[header.numParams - header.binderIds.size + i]!
let val elabTermEnsuringType valStx type
mkLambdaFVars xs val
headers.mapM fun header => do
let mut reusableResult? := none
if let some snap := header.bodySnap? then
if let some old := snap.old? then
-- guaranteed reusable as by the `bodySnap?` invariant, so let's wait on the previous
-- elaboration
if let some old := old.val.get then
snap.new.resolve <| some old
-- also make sure to reuse tactic snapshots if present so that body reuse does not lead to
-- missed tactic reuse on further changes
if let some tacSnap := header.tacSnap? then
if let some oldTacSnap := tacSnap.old? then
tacSnap.new.resolve oldTacSnap.val.get
reusableResult? := some (old.value, old.state)
withRestoreOrSaveFull reusableResult? fun save => do
withDeclName header.declName <| withLevelNames header.levelNames do
let valStx liftMacroM <| declValToTerm header.value
forallBoundedTelescope header.type header.numParams fun xs type => do
-- Add new info nodes for new fvars. The server will detect all fvars of a binder by the binder's source location.
for i in [0:header.binderIds.size] do
-- skip auto-bound prefix in `xs`
addLocalVarInfo header.binderIds[i]! xs[header.numParams - header.binderIds.size + i]!
let val withReader ({ · with tacSnap? := header.tacSnap? }) do
-- synthesize mvars here to force the top-level tactic block (if any) to run
elabTermEnsuringType valStx type <* synthesizeSyntheticMVarsNoPostponing
-- NOTE: without this `instantiatedMVars`, `mkLambdaFVars` may leave around a redex that
-- leads to more section variables being included than necessary
let val instantiateMVars val
let val mkLambdaFVars xs val
if let some snap := header.bodySnap? then
snap.new.resolve <| some {
diagnostics :=
( Language.Snapshot.Diagnostics.ofMessageLog ( Core.getAndEmptyMessageLog))
state := ( save)
value := val
}
return val
private def collectUsed (headers : Array DefViewElabHeader) (values : Array Expr) (toLift : List LetRecToLift)
: StateRefT CollectFVars.State MetaM Unit := do
@@ -640,7 +761,7 @@ def pushMain (preDefs : Array PreDefinition) (sectionVars : Array Expr) (mainHea
: TermElabM (Array PreDefinition) :=
mainHeaders.size.foldM (init := preDefs) fun i preDefs => do
let header := mainHeaders[i]!
let termination declValToTerminationHint header.valueStx
let termination declValToTerminationHint header.value
let termination := termination.rememberExtraParams header.numParams mainVals[i]!
let value mkLambdaFVars sectionVars mainVals[i]!
let type mkForallFVars sectionVars header.type
@@ -796,38 +917,40 @@ def elabMutualDef (vars : Array Expr) (views : Array DefView) : TermElabM Unit :
else
go
where
go := do
let scopeLevelNames getLevelNames
let headers elabHeaders views
let headers levelMVarToParamHeaders views headers
let allUserLevelNames := getAllUserLevelNames headers
withFunLocalDecls headers fun funFVars => do
for view in views, funFVar in funFVars do
addLocalVarInfo view.declId funFVar
let values
try
let values elabFunValues headers
Term.synthesizeSyntheticMVarsNoPostponing
values.mapM (instantiateMVars ·)
catch ex =>
logException ex
headers.mapM fun header => mkSorry header.type (synthetic := true)
let headers headers.mapM instantiateMVarsAtHeader
let letRecsToLift getLetRecsToLift
let letRecsToLift letRecsToLift.mapM instantiateMVarsAtLetRecToLift
checkLetRecsToLiftTypes funFVars letRecsToLift
withUsed vars headers values letRecsToLift fun vars => do
let preDefs MutualClosure.main vars headers funFVars values letRecsToLift
for preDef in preDefs do
trace[Elab.definition] "{preDef.declName} : {preDef.type} :=\n{preDef.value}"
let preDefs withLevelNames allUserLevelNames <| levelMVarToParamPreDecls preDefs
let preDefs instantiateMVarsAtPreDecls preDefs
let preDefs fixLevelParams preDefs scopeLevelNames allUserLevelNames
for preDef in preDefs do
trace[Elab.definition] "after eraseAuxDiscr, {preDef.declName} : {preDef.type} :=\n{preDef.value}"
checkForHiddenUnivLevels allUserLevelNames preDefs
addPreDefinitions preDefs
processDeriving headers
go :=
withAlwaysResolvedPromises views.size fun bodyPromises =>
withAlwaysResolvedPromises views.size fun tacPromises => do
let scopeLevelNames getLevelNames
let headers elabHeaders views bodyPromises tacPromises
let headers levelMVarToParamHeaders views headers
let allUserLevelNames := getAllUserLevelNames headers
withFunLocalDecls headers fun funFVars => do
for view in views, funFVar in funFVars do
addLocalVarInfo view.declId funFVar
let values
try
let values elabFunValues headers
Term.synthesizeSyntheticMVarsNoPostponing
values.mapM (instantiateMVars ·)
catch ex =>
logException ex
headers.mapM fun header => mkSorry header.type (synthetic := true)
let headers headers.mapM instantiateMVarsAtHeader
let letRecsToLift getLetRecsToLift
let letRecsToLift letRecsToLift.mapM instantiateMVarsAtLetRecToLift
checkLetRecsToLiftTypes funFVars letRecsToLift
withUsed vars headers values letRecsToLift fun vars => do
let preDefs MutualClosure.main vars headers funFVars values letRecsToLift
for preDef in preDefs do
trace[Elab.definition] "{preDef.declName} : {preDef.type} :=\n{preDef.value}"
let preDefs withLevelNames allUserLevelNames <| levelMVarToParamPreDecls preDefs
let preDefs instantiateMVarsAtPreDecls preDefs
let preDefs fixLevelParams preDefs scopeLevelNames allUserLevelNames
for preDef in preDefs do
trace[Elab.definition] "after eraseAuxDiscr, {preDef.declName} : {preDef.type} :=\n{preDef.value}"
checkForHiddenUnivLevels allUserLevelNames preDefs
addPreDefinitions preDefs
processDeriving headers
processDeriving (headers : Array DefViewElabHeader) := do
for header in headers, view in views do
@@ -842,12 +965,46 @@ end Term
namespace Command
def elabMutualDef (ds : Array Syntax) : CommandElabM Unit := do
let views ds.mapM fun d => do
let modifiers elabModifiers d[0]
if ds.size > 1 && modifiers.isNonrec then
throwErrorAt d "invalid use of 'nonrec' modifier in 'mutual' block"
mkDefView modifiers d[1]
runTermElabM fun vars => Term.elabMutualDef vars views
let opts getOptions
withAlwaysResolvedPromises ds.size fun headerPromises => do
let snap? := ( read).snap?
let mut views := #[]
let mut defs := #[]
let mut reusedAllHeaders := true
for h : i in [0:ds.size], headerPromise in headerPromises do
let d := ds[i]
let modifiers elabModifiers d[0]
if ds.size > 1 && modifiers.isNonrec then
throwErrorAt d "invalid use of 'nonrec' modifier in 'mutual' block"
let mut view mkDefView modifiers d[1]
let fullHeaderRef := mkNullNode #[d[0], view.headerRef]
if let some snap := snap? then
view := { view with headerSnap? := some {
old? := do
-- transitioning from `Context.snap?` to `DefView.headerSnap?` invariant: if the
-- elaboration context and state are unchanged, and the syntax of this as well as all
-- previous headers is unchanged, then the elaboration result for this header (which
-- includes state from elaboration of previous headers!) should be unchanged.
guard reusedAllHeaders
let old snap.old?
-- blocking wait, `HeadersParsedSnapshot` (and hopefully others) should be quick
let old old.val.get.toTyped? DefsParsedSnapshot
let oldParsed old.defs[i]?
guard <| fullHeaderRef.structRangeEqWithTraceReuse opts oldParsed.fullHeaderRef
-- no syntax guard to store, we already did the necessary checks
return .missing, oldParsed.headerProcessedSnap
new := headerPromise
} }
defs := defs.push {
fullHeaderRef
headerProcessedSnap := { range? := d.getRange?, task := headerPromise.result }
}
reusedAllHeaders := reusedAllHeaders && view.headerSnap?.any (·.old?.isSome)
views := views.push view
if let some snap := snap? then
-- no non-fatal diagnostics at this point
snap.new.resolve <| .ofTyped { defs, diagnostics := .empty : DefsParsedSnapshot }
runTermElabM fun vars => Term.elabMutualDef vars views
end Command
end Lean.Elab

View File

@@ -223,9 +223,12 @@ def getQuotKind (stx : Syntax) : TermElabM SyntaxNodeKind := do
| ``Parser.Tactic.quot => addNamedQuotInfo stx `tactic
| ``Parser.Tactic.quotSeq => addNamedQuotInfo stx `tactic.seq
| .str kind "quot" => addNamedQuotInfo stx kind
| ``dynamicQuot => match elabParserName stx[1] with
| ``dynamicQuot =>
let id := stx[1]
match ( elabParserName id) with
| .parser n _ => return n
| .category c => return c
| .alias _ => return ( Parser.getSyntaxKindOfParserAlias? id.getId.eraseMacroScopes).get!
| k => throwError "unexpected quotation kind {k}"
def mkSyntaxQuotation (stx : Syntax) (kind : Name) : TermElabM Syntax := do

View File

@@ -80,7 +80,7 @@ def checkLeftRec (stx : Syntax) : ToParserDescrM Bool := do
markAsTrailingParser (prec?.getD 0)
return true
def elabParserName? (stx : Syntax.Ident) : TermElabM (Option Parser.ParserName) := do
def elabParserName? (stx : Syntax.Ident) : TermElabM (Option Parser.ParserResolution) := do
match Parser.resolveParserName stx with
| [n@(.category cat)] =>
addCategoryInfo stx cat
@@ -88,10 +88,12 @@ def elabParserName? (stx : Syntax.Ident) : TermElabM (Option Parser.ParserName)
| [n@(.parser parser _)] =>
addTermInfo' stx (Lean.mkConst parser)
return n
| [n@(.alias _)] =>
return n
| _::_::_ => throwErrorAt stx "ambiguous parser {stx}"
| [] => return none
def elabParserName (stx : Syntax.Ident) : TermElabM Parser.ParserName := do
def elabParserName (stx : Syntax.Ident) : TermElabM Parser.ParserResolution := do
match elabParserName? stx with
| some n => return n
| none => throwErrorAt stx "unknown parser {stx}"
@@ -194,12 +196,6 @@ where
processNullaryOrCat (stx : Syntax) := do
let ident := stx[0]
let id := ident.getId.eraseMacroScopes
-- run when parser is neither a decl nor a cat
let default := do
if ( Parser.isParserAlias id) then
ensureNoPrec stx
return ( processAlias ident #[])
throwError "unknown parser declaration/category/alias '{id}'"
match ( elabParserName? ident) with
| some (.parser c (isDescr := true)) =>
ensureNoPrec stx
@@ -209,14 +205,18 @@ where
| some (.parser c (isDescr := false)) =>
if ( Parser.getParserAliasInfo id).declName == c then
-- prefer parser alias over base declaration because it has more metadata, #2249
return ( default)
ensureNoPrec stx
return ( processAlias ident #[])
ensureNoPrec stx
-- as usual, we assume that people using `Parser` know what they are doing
let stackSz := 1
return ( `(ParserDescr.parser $(quote c)), stackSz)
| some (.category _) =>
processParserCategory stx
| none => default
| some (.alias _) =>
ensureNoPrec stx
processAlias ident #[]
| none => throwError "unknown parser declaration/category/alias '{id}'"
processSepBy (stx : Syntax) := do
let p ensureUnaryOutput <$> withNestedParser do process stx[1]

View File

@@ -324,7 +324,6 @@ mutual
If `report := false`, then `runTactic` will not capture exceptions nor will report unsolved goals. Unsolved goals become exceptions.
-/
partial def runTactic (mvarId : MVarId) (tacticCode : Syntax) (report := true) : TermElabM Unit := withoutAutoBoundImplicit do
let code := tacticCode[1]
instantiateMVarDeclMVars mvarId
/-
TODO: consider using `runPendingTacticsAt` at `mvarId` local context and target type.
@@ -346,7 +345,7 @@ mutual
-- also put an info node on the `by` keyword specifically -- the token may be `canonical` and thus shown in the info
-- view even though it is synthetic while a node like `tacticCode` never is (#1990)
withTacticInfoContext tacticCode[0] do
evalTactic code
withNarrowedArgTacticReuse (argIdx := 1) (evalTactic ·) tacticCode
synthesizeSyntheticMVars (postpone := .no)
unless remainingGoals.isEmpty do
if report then

View File

@@ -34,10 +34,6 @@ structure Context where
-/
recover : Bool := true
structure SavedState where
term : Term.SavedState
tactic : State
abbrev TacticM := ReaderT Context $ StateRefT State TermElabM
abbrev Tactic := Syntax TacticM Unit
@@ -100,6 +96,16 @@ def SavedState.restore (b : SavedState) (restoreInfo := false) : TacticM Unit :=
b.term.restore restoreInfo
set b.tactic
@[specialize, inherit_doc Core.withRestoreOrSaveFull]
def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState))
(cont : TacticM SavedState TacticM α) : TacticM α := do
if let some (_, state) := reusableResult? then
set state.tactic
let reusableResult? := reusableResult?.map (fun (val, state) => (val, state.term))
controlAt TermElabM fun runInBase =>
Term.withRestoreOrSaveFull reusableResult? fun restore =>
runInBase <| cont (return { term := ( restore), tactic := ( get) })
protected def getCurrMacroScope : TacticM MacroScope := do pure ( readThe Core.Context).currMacroScope
protected def getMainModule : TacticM Name := do pure ( getEnv).mainModule
@@ -146,7 +152,10 @@ partial def evalTactic (stx : Syntax) : TacticM Unit := do
| .node _ k _ =>
if k == nullKind then
-- Macro writers create a sequence of tactics `t₁ ... tₙ` using `mkNullNode #[t₁, ..., tₙ]`
stx.getArgs.forM evalTactic
-- We could support incrementality here by allocating `n` new snapshot bundles but the
-- practical value is not clear
Term.withoutTacticIncrementality true do
stx.getArgs.forM evalTactic
else withTraceNode `Elab.step (fun _ => return stx) (tag := stx.getKind.toString) do
let evalFns := tacticElabAttribute.getEntries ( getEnv) stx.getKind
let macros := macroAttribute.getEntries ( getEnv) stx.getKind
@@ -200,7 +209,11 @@ where
| [] => throwExs failures
| evalFn::evalFns => do
try
withReader ({ · with elaborator := evalFn.declName }) <| withTacticInfoContext stx <| evalFn.value stx
-- prevent unsupported tactics from accidentally accessing `Term.Context.tacSnap?`
Term.withoutTacticIncrementality (!( isIncrementalElab evalFn.declName)) do
withReader ({ · with elaborator := evalFn.declName }) do
withTacticInfoContext stx do
evalFn.value stx
catch ex => handleEx s failures ex (eval s evalFns)
def throwNoGoalsToBeSolved : TacticM α :=

View File

@@ -29,13 +29,90 @@ open Parser.Tactic
@[builtin_tactic Lean.Parser.Tactic.«done»] def evalDone : Tactic := fun _ =>
done
@[builtin_tactic seq1] def evalSeq1 : Tactic := fun stx => do
let args := stx[0].getArgs
for i in [:args.size] do
if i % 2 == 0 then
evalTactic args[i]!
else
saveTacticInfoForToken args[i]! -- add `TacticInfo` node for `;`
open Language in
/--
Evaluates a tactic script in form of a syntax node with alternating tactics and separators as
children.
-/
partial def evalSepTactics : Tactic := goEven
where
-- `stx[0]` is the next tactic step, if any
goEven stx := do
if stx.getNumArgs == 0 then
return
let tac := stx[0]
/-
Each `goEven` step creates three promises under incrementality and reuses their older versions
where possible:
* `finished` is resolved when `tac` finishes execution; if `tac` is wholly unchanged from the
previous version, its state is reused and `tac` execution is skipped. Note that this promise
is never turned into a `SnapshotTask` and added to the snapshot tree as incremental reporting
is already covered by the next two promises.
* `inner` is passed to `tac` if it is marked as supporting incrementality and can be used for
reporting and partial reuse inside of it; if the tactic is unsupported or `finished` is wholly
reused, it is ignored.
* `next` is used as the context when invoking `goOdd` and thus eventually used for the next
`goEven` step. Thus, the incremental state of a tactic script is ultimately represented as a
chain of `next` snapshots. Its reuse is disabled if `tac` or its following separator are
changed in any way.
-/
let mut oldInner? := none
if let some snap := ( readThe Term.Context).tacSnap? then
if let some old := snap.old? then
let oldParsed := old.val.get
oldInner? := oldParsed.next.get? 0 |>.map (oldParsed.data.stx, ·)
-- compare `stx[0]` for `finished`/`next` reuse, focus on remainder of script
Term.withNarrowedTacticReuse (stx := stx) (fun stx => (stx[0], mkNullNode stx.getArgs[1:])) fun stxs => do
let some snap := ( readThe Term.Context).tacSnap?
| do evalTactic tac; goOdd stxs
let mut reusableResult? := none
let mut oldNext? := none
if let some old := snap.old? then
-- `tac` must be unchanged given the narrow above; let's reuse `finished`'s state!
let oldParsed := old.val.get
if let some state := oldParsed.data.finished.get.state? then
reusableResult? := some (state, state)
-- only allow `next` reuse in this case
oldNext? := oldParsed.next.get? 1 |>.map (old.stx, ·)
withAlwaysResolvedPromise fun next => do
withAlwaysResolvedPromise fun finished => do
withAlwaysResolvedPromise fun inner => do
snap.new.resolve <| .mk {
stx := tac
diagnostics := ( Language.Snapshot.Diagnostics.ofMessageLog
( Core.getAndEmptyMessageLog))
finished := finished.result
} #[
{
range? := tac.getRange?
task := inner.result },
{
range? := stxs |>.getRange?
task := next.result }]
let state withRestoreOrSaveFull reusableResult? fun save => do
-- set up nested reuse; `evalTactic` will check for `isIncrementalElab`
withTheReader Term.Context ({ · with
tacSnap? := some { old? := oldInner?, new := inner } }) do
evalTactic tac
save
finished.resolve { state? := state }
withTheReader Term.Context ({ · with tacSnap? := some {
new := next
old? := oldNext?
} }) do
goOdd stxs
-- `stx[0]` is the next separator, if any
goOdd stx := do
if stx.getNumArgs == 0 then
return
saveTacticInfoForToken stx[0] -- add `TacticInfo` node for `;`
-- disable further reuse on separator change as to not reuse wrong `TacticInfo`
Term.withNarrowedTacticReuse (fun stx => (stx[0], mkNullNode stx.getArgs[1:])) goEven stx
@[builtin_tactic seq1] def evalSeq1 : Tactic := fun stx =>
evalSepTactics stx[0]
@[builtin_tactic paren] def evalParen : Tactic := fun stx =>
evalTactic stx[1]
@@ -104,26 +181,20 @@ def addCheckpoints (stx : Syntax) : TacticM Syntax := do
output := output ++ currentCheckpointBlock
return stx.setArgs output
/-- Evaluate `sepByIndent tactic "; " -/
def evalSepByIndentTactic (stx : Syntax) : TacticM Unit := do
let stx addCheckpoints stx
for arg in stx.getArgs, i in [:stx.getArgs.size] do
if i % 2 == 0 then
evalTactic arg
else
saveTacticInfoForToken arg
@[builtin_tactic tacticSeq1Indented, builtin_incremental]
def evalTacticSeq1Indented : Tactic :=
Term.withNarrowedArgTacticReuse (argIdx := 0) evalSepTactics
@[builtin_tactic tacticSeq1Indented] def evalTacticSeq1Indented : Tactic := fun stx =>
evalSepByIndentTactic stx[0]
@[builtin_tactic tacticSeqBracketed] def evalTacticSeqBracketed : Tactic := fun stx => do
@[builtin_tactic tacticSeqBracketed, builtin_incremental]
def evalTacticSeqBracketed : Tactic := fun stx => do
let initInfo mkInitialTacticInfo stx[0]
withRef stx[2] <| closeUsingOrAdmit do
-- save state before/after entering focus on `{`
withInfoContext (pure ()) initInfo
evalSepByIndentTactic stx[1]
Term.withNarrowedArgTacticReuse (argIdx := 1) evalSepTactics stx
@[builtin_tactic Lean.cdot] def evalTacticCDot : Tactic := fun stx => do
@[builtin_tactic Lean.cdot, builtin_incremental]
def evalTacticCDot : Tactic := fun stx => do
-- adjusted copy of `evalTacticSeqBracketed`; we used to use the macro
-- ``| `(tactic| $cdot:cdotTk $tacs) => `(tactic| {%$cdot ($tacs) }%$cdot)``
-- but the token antiquotation does not copy trailing whitespace, leading to
@@ -132,7 +203,7 @@ def evalSepByIndentTactic (stx : Syntax) : TacticM Unit := do
withRef stx[0] <| closeUsingOrAdmit do
-- save state before/after entering focus on `·`
withInfoContext (pure ()) initInfo
evalSepByIndentTactic stx[1]
Term.withNarrowedArgTacticReuse (argIdx := 1) evalTactic stx
@[builtin_tactic Parser.Tactic.focus] def evalFocus : Tactic := fun stx => do
let mkInfo mkInitialTacticInfo stx[0]
@@ -205,8 +276,9 @@ private def getOptRotation (stx : Syntax) : Nat :=
throwError "failed on all goals"
setGoals mvarIdsNew.toList
@[builtin_tactic tacticSeq] def evalTacticSeq : Tactic := fun stx =>
evalTactic stx[0]
@[builtin_tactic tacticSeq, builtin_incremental]
def evalTacticSeq : Tactic :=
Term.withNarrowedArgTacticReuse (argIdx := 0) evalTactic
partial def evalChoiceAux (tactics : Array Syntax) (i : Nat) : TacticM Unit :=
if h : i < tactics.size then
@@ -392,7 +464,7 @@ def renameInaccessibles (mvarId : MVarId) (hs : TSyntaxArray ``binderIdent) : Ta
private def getCaseGoals (tag : TSyntax ``binderIdent) : TacticM (MVarId × List MVarId) := do
let gs getUnsolvedGoals
let g if let `(binderIdent| $tag:ident) := tag then
let tag := tag.getId
let tag := tag.getId.eraseMacroScopes
let some g findTag? gs tag | notFound gs tag
pure g
else
@@ -426,16 +498,16 @@ where
.group <| .nest 2 <|
.ofFormat .line ++ .joinSep items sep
@[builtin_tactic «case»] def evalCase : Tactic
| stx@`(tactic| case $[$tag $hs*]|* =>%$arr $tac:tacticSeq) =>
@[builtin_tactic «case», builtin_incremental]
def evalCase : Tactic
| stx@`(tactic| case $[$tag $hs*]|* =>%$arr $tac:tacticSeq1Indented) =>
for tag in tag, hs in hs do
let (g, gs) getCaseGoals tag
let g renameInaccessibles g hs
setGoals [g]
g.setTag Name.anonymous
withCaseRef arr tac do
closeUsingOrAdmit (withTacticInfoContext stx (evalTactic tac))
withCaseRef arr tac <| closeUsingOrAdmit <| withTacticInfoContext stx <|
Term.withNarrowedArgTacticReuse (argIdx := 3) (evalTactic ·) stx
setGoals gs
| _ => throwUnsupportedSyntax
@@ -499,7 +571,7 @@ where
match stx with
| `(tactic| replace $decl:haveDecl) =>
withMainContext do
let vars Elab.Term.Do.getDoHaveVars <| mkNullNode #[.missing, decl]
let vars Elab.Term.Do.getDoHaveVars ( `(doElem| have $decl:haveDecl))
let origLCtx getLCtx
evalTactic $ `(tactic| have $decl:haveDecl)
let mut toClear := #[]

View File

@@ -54,23 +54,25 @@ private def getAltDArrow (alt : Syntax) : Syntax :=
def isHoleRHS (rhs : Syntax) : Bool :=
rhs.isOfKind ``Parser.Term.syntheticHole || rhs.isOfKind ``Parser.Term.hole
def evalAlt (mvarId : MVarId) (alt : Syntax) (addInfo : TermElabM Unit) (remainingGoals : Array MVarId) : TacticM (Array MVarId) :=
def evalAlt (mvarId : MVarId) (alt : Syntax) (addInfo : TermElabM Unit) : TacticM Unit :=
let rhs := getAltRHS alt
withCaseRef (getAltDArrow alt) rhs do
if isHoleRHS rhs then
addInfo
let gs' mvarId.withContext <| withTacticInfoContext rhs do
mvarId.withContext <| withTacticInfoContext rhs do
let mvarDecl mvarId.getDecl
let val elabTermEnsuringType rhs mvarDecl.type
mvarId.assign val
let gs' getMVarsNoDelayed val
tagUntaggedGoals mvarDecl.userName `induction gs'.toList
pure gs'
return remainingGoals ++ gs'
setGoals <| ( getGoals) ++ gs'.toList
else
setGoals [mvarId]
closeUsingOrAdmit (withTacticInfoContext alt (addInfo *> evalTactic rhs))
return remainingGoals
let goals getGoals
try
setGoals [mvarId]
closeUsingOrAdmit (withTacticInfoContext alt (addInfo *> evalTactic rhs))
finally
setGoals goals
/-!
Helper method for creating an user-defined eliminator/recursor application.
@@ -199,6 +201,9 @@ private def getAltNumFields (elimInfo : ElimInfo) (altName : Name) : TermElabM N
return altInfo.numFields
throwError "unknown alternative name '{altName}'"
private def isWildcard (altStx : Syntax) : Bool :=
getAltName altStx == `_
private def checkAltNames (alts : Array Alt) (altsSyntax : Array Syntax) : TacticM Unit :=
for i in [:altsSyntax.size] do
let altStx := altsSyntax[i]!
@@ -229,151 +234,184 @@ private def saveAltVarsInfo (altMVarId : MVarId) (altStx : Syntax) (fvarIds : Ar
Term.addLocalVarInfo altVars[i]! (mkFVar fvarId)
i := i + 1
/--
If `altsSyntax` is not empty we reorder `alts` using the order the alternatives have been provided
in `altsSyntax`. Motivations:
1- It improves the effectiveness of the `checkpoint` and `save` tactics. Consider the following example:
```lean
example (h₁ : p q) (h₂ : p → x = 0) (h₃ : q → y = 0) : x * y = 0 := by
cases h₁ with
| inr h =>
sleep 5000 -- sleeps for 5 seconds
save
have : y = 0 := h₃ h
-- We can confortably work here
| inl h => stop ...
```
If we do reorder, the `inl` alternative will be executed first. Moreover, as we type in the `inr` alternative,
type errors will "swallow" the `inl` alternative and affect the tactic state at `save` making it ineffective.
2- The errors are produced in the same order the appear in the code above. This is not super important when using IDEs.
-/
def reorderAlts (alts : Array Alt) (altsSyntax : Array Syntax) : Array Alt := Id.run do
if altsSyntax.isEmpty then
return alts
else
let mut alts := alts
let mut result := #[]
for altStx in altsSyntax do
let altName := getAltName altStx
let some i := alts.findIdx? (·.1 == altName) | return result ++ alts
result := result.push alts[i]!
alts := alts.eraseIdx i
return result ++ alts
def evalAlts (elimInfo : ElimInfo) (alts : Array Alt) (optPreTac : Syntax) (altsSyntax : Array Syntax)
open Language in
def evalAlts (elimInfo : ElimInfo) (alts : Array Alt) (optPreTac : Syntax) (altStxs : Array Syntax)
(initialInfo : Info)
(numEqs : Nat := 0) (numGeneralized : Nat := 0) (toClear : Array FVarId := #[])
(toTag : Array (Ident × FVarId) := #[]) : TacticM Unit := do
let hasAlts := altsSyntax.size > 0
let hasAlts := altStxs.size > 0
if hasAlts then
-- default to initial state outside of alts
-- HACK: because this node has the same span as the original tactic,
-- we need to take all the info trees we have produced so far and re-nest them
-- inside this node as well
let treesSaved getResetInfoTrees
withInfoContext ((modifyInfoState fun s => { s with trees := treesSaved }) *> go) (pure initialInfo)
else go
withInfoContext ((modifyInfoState fun s => { s with trees := treesSaved }) *> goWithInfo) (pure initialInfo)
else goWithInfo
where
go := do
checkAltNames alts altsSyntax
let alts := reorderAlts alts altsSyntax
let hasAlts := altsSyntax.size > 0
let mut usedWildcard := false
let mut subgoals := #[] -- when alternatives are not provided, we accumulate subgoals here
let mut altsSyntax := altsSyntax
-- continuation in the correct info context
goWithInfo := do
let hasAlts := altStxs.size > 0
if hasAlts then
if let some tacSnap := ( readThe Term.Context).tacSnap? then
-- incrementality: create a new promise for each alternative, resolve current snapshot to
-- them, eventually put each of them back in `Context.tacSnap?` in `applyAltStx`
withAlwaysResolvedPromise fun finished => do
withAlwaysResolvedPromises altStxs.size fun altPromises => do
tacSnap.new.resolve <| .mk {
-- save all relevant syntax here for comparison with next document version
stx := mkNullNode altStxs
diagnostics := .empty
finished := finished.result
} (altStxs.zipWith altPromises fun stx prom =>
{ range? := stx.getRange?, task := prom.result })
goWithIncremental <| altPromises.mapIdx fun i prom => {
old? := do
let old tacSnap.old?
-- waiting is fine here: this is the old version of the snapshot resolved above
-- immediately at the beginning of the tactic
let old := old.val.get
-- use old version of `mkNullNode altsSyntax` as guard, will be compared with new
-- version and picked apart in `applyAltStx`
return old.data.stx, ( old.next[i]?)
new := prom
}
finished.resolve { state? := ( saveState) }
return
goWithIncremental #[]
-- continuation in the correct incrementality context
goWithIncremental (tacSnaps : Array (SnapshotBundle TacticParsedSnapshot)) := do
let hasAlts := altStxs.size > 0
let mut alts := alts
-- initial sanity checks: named cases should be known, wildcards should be last
checkAltNames alts altStxs
/-
First process `altsSyntax` in order, removing covered alternatives from `alts`. Previously we
did one loop through `alts`, looking up suitable alternatives from `altsSyntax`.
Motivations for the change:
1- It improves the effectiveness of incremental reuse. Consider the following example:
```lean
example (h₁ : p q) (h₂ : p → x = 0) (h₃ : q → y = 0) : x * y = 0 := by
cases h₁ with
| inr h =>
sleep 5000 -- sleeps for 5 seconds
save
have : y = 0 := h₃ h
-- We can comfortably work here
| inl h => stop ...
```
If we iterated through `alts` instead of `altsSyntax`, the `inl` alternative would be executed
first, making partial reuse in `inr` impossible (without support for reuse with position
adjustments).
2- The errors are produced in the same order the appear in the code above. This is not super
important when using IDEs.
-/
for altStxIdx in [0:altStxs.size] do
let altStx := altStxs[altStxIdx]!
let altName := getAltName altStx
if let some i := alts.findIdx? (·.1 == altName) then
-- cover named alternative
applyAltStx tacSnaps altStxIdx altStx alts[i]!
alts := alts.eraseIdx i
else if !alts.isEmpty && isWildcard altStx then
-- cover all alternatives
for alt in alts do
applyAltStx tacSnaps altStxIdx altStx alt
alts := #[]
else
throwErrorAt altStx "unused alternative '{altName}'"
-- now process remaining alternatives; these might either be unreachable or we're in `induction`
-- without `with`. In all other cases, remaining alternatives are flagged as errors.
for { name := altName, info, mvarId := altMVarId } in alts do
let numFields getAltNumFields elimInfo altName
let mut isWildcard := false
let altStx?
match altsSyntax.findIdx? (fun alt => getAltName alt == altName) with
| some idx =>
let altStx := altsSyntax[idx]!
altsSyntax := altsSyntax.eraseIdx idx
pure (some altStx)
| none => match altsSyntax.findIdx? (fun alt => getAltName alt == `_) with
| some idx =>
isWildcard := true
pure (some altsSyntax[idx]!)
| none =>
pure none
match altStx? with
| none =>
let mut (_, altMVarId) altMVarId.introN numFields
match ( Cases.unifyEqs? numEqs altMVarId {}) with
| none => pure () -- alternative is not reachable
| some (altMVarId', subst) =>
altMVarId if info.provesMotive then
(_, altMVarId) altMVarId'.introNP numGeneralized
pure altMVarId
else
pure altMVarId'
for fvarId in toClear do
altMVarId altMVarId.tryClear fvarId
altMVarId.withContext do
for (stx, fvar) in toTag do
Term.addLocalVarInfo stx (subst.get fvar)
let altMVarIds applyPreTac altMVarId
if !hasAlts then
-- User did not provide alternatives using `|`
subgoals := subgoals ++ altMVarIds.toArray
else if altMVarIds.isEmpty then
pure ()
else
logError m!"alternative '{altName}' has not been provided"
altMVarIds.forM fun mvarId => admitGoal mvarId
| some altStx =>
(subgoals, usedWildcard) withRef altStx do
let altVars := getAltVars altStx
let numFieldsToName if altHasExplicitModifier altStx then pure numFields else getNumExplicitFields altMVarId numFields
if altVars.size > numFieldsToName then
logError m!"too many variable names provided at alternative '{altName}', #{altVars.size} provided, but #{numFieldsToName} expected"
let mut (fvarIds, altMVarId) altMVarId.introN numFields (altVars.toList.map getNameOfIdent') (useNamesForExplicitOnly := !altHasExplicitModifier altStx)
-- Delay adding the infos for the pattern LHS because we want them to nest
-- inside tacticInfo for the current alternative (in `evalAlt`)
let addInfo : TermElabM Unit := do
if ( getInfoState).enabled then
if let some declName := info.declName? then
addConstInfo (getAltNameStx altStx) declName
saveAltVarsInfo altMVarId altStx fvarIds
let unusedAlt := do
addInfo
if isWildcard then
pure (#[], usedWildcard)
else
throwError "alternative '{altName}' is not needed"
match ( Cases.unifyEqs? numEqs altMVarId {}) with
| none => unusedAlt
| some (altMVarId', subst) =>
altMVarId if info.provesMotive then
(_, altMVarId) altMVarId'.introNP numGeneralized
pure altMVarId
else
pure altMVarId'
for fvarId in toClear do
altMVarId altMVarId.tryClear fvarId
altMVarId.withContext do
for (stx, fvar) in toTag do
Term.addLocalVarInfo stx (subst.get fvar)
let altMVarIds applyPreTac altMVarId
if altMVarIds.isEmpty then
unusedAlt
else
let mut subgoals := subgoals
for altMVarId' in altMVarIds do
subgoals evalAlt altMVarId' altStx addInfo subgoals
pure (subgoals, usedWildcard || isWildcard)
if usedWildcard then
altsSyntax := altsSyntax.filter fun alt => getAltName alt != `_
unless altsSyntax.isEmpty do
logErrorAt altsSyntax[0]! "unused alternative"
setGoals subgoals.toList
let mut (_, altMVarId) altMVarId.introN numFields
let some (altMVarId', subst) Cases.unifyEqs? numEqs altMVarId {}
| continue -- alternative is not reachable
altMVarId if info.provesMotive then
(_, altMVarId) altMVarId'.introNP numGeneralized
pure altMVarId
else
pure altMVarId'
for fvarId in toClear do
altMVarId altMVarId.tryClear fvarId
altMVarId.withContext do
for (stx, fvar) in toTag do
Term.addLocalVarInfo stx (subst.get fvar)
let altMVarIds applyPreTac altMVarId
if !hasAlts then
-- User did not provide alternatives using `|`
setGoals <| ( getGoals) ++ altMVarIds
else if !altMVarIds.isEmpty then
logError m!"alternative '{altName}' has not been provided"
altMVarIds.forM fun mvarId => admitGoal mvarId
/-- Applies syntactic alternative to alternative goal. -/
applyAltStx tacSnaps altStxIdx altStx alt := withRef altStx do
let { name := altName, info, mvarId := altMVarId } := alt
-- also checks for unknown alternatives
let numFields getAltNumFields elimInfo altName
let altVars := getAltVars altStx
let numFieldsToName if altHasExplicitModifier altStx then pure numFields else getNumExplicitFields altMVarId numFields
if altVars.size > numFieldsToName then
logError m!"too many variable names provided at alternative '{altName}', #{altVars.size} provided, but #{numFieldsToName} expected"
let mut (fvarIds, altMVarId) altMVarId.introN numFields (altVars.toList.map getNameOfIdent') (useNamesForExplicitOnly := !altHasExplicitModifier altStx)
-- Delay adding the infos for the pattern LHS because we want them to nest
-- inside tacticInfo for the current alternative (in `evalAlt`)
let addInfo : TermElabM Unit := do
if ( getInfoState).enabled then
if let some declName := info.declName? then
addConstInfo (getAltNameStx altStx) declName
saveAltVarsInfo altMVarId altStx fvarIds
let unusedAlt := do
addInfo
if !isWildcard altStx then
throwError "alternative '{altName}' is not needed"
let some (altMVarId', subst) Cases.unifyEqs? numEqs altMVarId {}
| unusedAlt
altMVarId if info.provesMotive then
(_, altMVarId) altMVarId'.introNP numGeneralized
pure altMVarId
else
pure altMVarId'
for fvarId in toClear do
altMVarId altMVarId.tryClear fvarId
altMVarId.withContext do
for (stx, fvar) in toTag do
Term.addLocalVarInfo stx (subst.get fvar)
let altMVarIds applyPreTac altMVarId
if altMVarIds.isEmpty then
return ( unusedAlt)
-- select corresponding snapshot bundle for incrementality of this alternative
-- note that `tacSnaps[altStxIdx]?` is `none` if `tacSnap?` was `none` to begin with
withTheReader Term.Context ({ · with tacSnap? := tacSnaps[altStxIdx]? }) do
-- all previous alternatives have to be unchanged for reuse
Term.withNarrowedArgTacticReuse (stx := mkNullNode altStxs) (argIdx := altStxIdx) fun altStx => do
-- everything up to rhs has to be unchanged for reuse
Term.withNarrowedArgTacticReuse (stx := altStx) (argIdx := 2) fun _rhs => do
-- disable reuse if rhs is run multiple times
Term.withoutTacticIncrementality (altMVarIds.length != 1 || isWildcard altStx) do
for altMVarId' in altMVarIds do
evalAlt altMVarId' altStx addInfo
/-- Applies `induction .. with $preTac | ..`, if any, to an alternative goal. -/
applyPreTac (mvarId : MVarId) : TacticM (List MVarId) :=
if optPreTac.isNone then
return [mvarId]
else
evalTacticAt optPreTac[0] mvarId
-- disable incrementality for the pre-tactic to avoid non-monotonic progress reporting; it
-- would be possible to include a custom task around the pre-tac with an appropriate range in
-- the snapshot such that it is cached as well if it turns out that this is valuable
Term.withoutTacticIncrementality true do
evalTacticAt optPreTac[0] mvarId
end ElimApp
@@ -420,8 +458,24 @@ Return an array containing its alternatives.
private def getAltsOfInductionAlts (inductionAlts : Syntax) : Array Syntax :=
inductionAlts[2].getArgs
private def getAltsOfOptInductionAlts (optInductionAlts : Syntax) : Array Syntax :=
if optInductionAlts.isNone then #[] else getAltsOfInductionAlts optInductionAlts[0]
/--
Given `inductionAlts` of the form
```
syntax inductionAlts := "with " (tactic)? withPosition( (colGe inductionAlt)+)
```
runs `cont alts` where `alts` is an array containing all `inductionAlt`s while disabling incremental
reuse if any other syntax changed.
-/
private def withAltsOfOptInductionAlts (optInductionAlts : Syntax)
(cont : Array Syntax TacticM α) : TacticM α :=
Term.withNarrowedTacticReuse (stx := optInductionAlts) (fun optInductionAlts =>
if optInductionAlts.isNone then
-- if there are no alternatives, what to compare is irrelevant as there will be no reuse
(mkNullNode #[], mkNullNode #[])
else
-- `with` and tactic applied to all branches must be unchanged for reuse
(mkNullNode optInductionAlts[0].getArgs[:2], optInductionAlts[0].getArg 2))
(fun alts => cont alts.getArgs)
private def getOptPreTacOfOptInductionAlts (optInductionAlts : Syntax) : Syntax :=
if optInductionAlts.isNone then mkNullNode else optInductionAlts[0][1]
@@ -582,12 +636,11 @@ private def generalizeTargets (exprs : Array Expr) : TacticM (Array Expr) := do
else
return exprs
@[builtin_tactic Lean.Parser.Tactic.induction] def evalInduction : Tactic := fun stx =>
@[builtin_tactic Lean.Parser.Tactic.induction, builtin_incremental]
def evalInduction : Tactic := fun stx =>
match expandInduction? stx with
| some stxNew => withMacroExpansion stx stxNew <| evalTactic stxNew
| _ => focus do
let optInductionAlts := stx[4]
let alts := getAltsOfOptInductionAlts optInductionAlts
let targets withMainContext <| stx[1].getSepArgs.mapM (elabTerm · none)
let targets generalizeTargets targets
let elimInfo withMainContext <| getElimNameInfo stx[2] targets (induction := true)
@@ -605,10 +658,15 @@ private def generalizeTargets (exprs : Array Expr) : TacticM (Array Expr) := do
ElimApp.mkElimApp elimInfo targets tag
trace[Elab.induction] "elimApp: {result.elimApp}"
ElimApp.setMotiveArg mvarId result.motive targetFVarIds
let optPreTac := getOptPreTacOfOptInductionAlts optInductionAlts
mvarId.assign result.elimApp
ElimApp.evalAlts elimInfo result.alts optPreTac alts initInfo (numGeneralized := n) (toClear := targetFVarIds)
appendGoals result.others.toList
-- drill down into old and new syntax: allow reuse of an rhs only if everything before it is
-- unchanged
-- everything up to the alternatives must be unchanged for reuse
Term.withNarrowedArgTacticReuse (stx := stx) (argIdx := 4) fun optInductionAlts => do
withAltsOfOptInductionAlts optInductionAlts fun alts => do
let optPreTac := getOptPreTacOfOptInductionAlts optInductionAlts
mvarId.assign result.elimApp
ElimApp.evalAlts elimInfo result.alts optPreTac alts initInfo (numGeneralized := n) (toClear := targetFVarIds)
appendGoals result.others.toList
where
checkTargets (targets : Array Expr) : MetaM Unit := do
let mut foundFVars : FVarIdSet := {}
@@ -650,15 +708,13 @@ def elabCasesTargets (targets : Array Syntax) : TacticM (Array Expr × Array (Id
else
return (args.map (·.expr), #[])
@[builtin_tactic Lean.Parser.Tactic.cases] def evalCases : Tactic := fun stx =>
@[builtin_tactic Lean.Parser.Tactic.cases, builtin_incremental]
def evalCases : Tactic := fun stx =>
match expandCases? stx with
| some stxNew => withMacroExpansion stx stxNew <| evalTactic stxNew
| _ => focus do
-- leading_parser nonReservedSymbol "cases " >> sepBy1 (group majorPremise) ", " >> usingRec >> optInductionAlts
let (targets, toTag) elabCasesTargets stx[1].getSepArgs
let optInductionAlts := stx[3]
let optPreTac := getOptPreTacOfOptInductionAlts optInductionAlts
let alts := getAltsOfOptInductionAlts optInductionAlts
let targetRef := stx[1]
let elimInfo withMainContext <| getElimNameInfo stx[2] targets (induction := false)
let mvarId getMainGoal
@@ -676,8 +732,14 @@ def elabCasesTargets (targets : Array Syntax) : TacticM (Array Expr × Array (Id
mvarId.withContext do
ElimApp.setMotiveArg mvarId elimArgs[elimInfo.motivePos]!.mvarId! targetsNew
mvarId.assign result.elimApp
ElimApp.evalAlts elimInfo result.alts optPreTac alts initInfo
(numEqs := targets.size) (toClear := targetsNew) (toTag := toTag)
-- drill down into old and new syntax: allow reuse of an rhs only if everything before it is
-- unchanged
-- everything up to the alternatives must be unchanged for reuse
Term.withNarrowedArgTacticReuse (stx := stx) (argIdx := 3) fun optInductionAlts => do
withAltsOfOptInductionAlts optInductionAlts fun alts => do
let optPreTac := getOptPreTacOfOptInductionAlts optInductionAlts
ElimApp.evalAlts elimInfo result.alts optPreTac alts initInfo
(numEqs := targets.size) (toClear := targetsNew) (toTag := toTag)
builtin_initialize
registerTraceClass `Elab.cases

View File

@@ -532,7 +532,9 @@ Helpful error message when omega cannot find a solution
def formatErrorMessage (p : Problem) : OmegaM MessageData := do
if p.possible then
if p.isEmpty then
return m!"it is false"
return m!"No usable constraints found. You may need to unfold definitions so `omega` can see \
linear arithmetic facts about `Nat` and `Int`, which may also involve multiplication, \
division, and modular remainder by constants."
else
let as atoms
let mask mentioned p.constraints

View File

@@ -153,6 +153,7 @@ inductive ResolveSimpIdResult where
Elaborate extra simp theorems provided to `simp`. `stx` is of the form `"[" simpTheorem,* "]"`
If `eraseLocal == true`, then we consider local declarations when resolving names for erased theorems (`- id`),
this option only makes sense for `simp_all` or `*` is used.
Try to recover from errors as much as possible so that users keep seeing the current goal.
-/
def elabSimpArgs (stx : Syntax) (ctx : Simp.Context) (simprocs : Simp.SimprocsArray) (eraseLocal : Bool) (kind : SimpKind) : TacticM ElabSimpArgsResult := do
if stx.isNone then
@@ -171,56 +172,58 @@ def elabSimpArgs (stx : Syntax) (ctx : Simp.Context) (simprocs : Simp.SimprocsAr
let mut simprocs := simprocs
let mut starArg := false
for arg in stx[1].getSepArgs do
if arg.getKind == ``Lean.Parser.Tactic.simpErase then
let fvar if eraseLocal || starArg then Term.isLocalIdent? arg[1] else pure none
if let some fvar := fvar then
-- We use `eraseCore` because the simp theorem for the hypothesis was not added yet
thms := thms.eraseCore (.fvar fvar.fvarId!)
try -- like withLogging, but compatible with do-notation
if arg.getKind == ``Lean.Parser.Tactic.simpErase then
let fvar? if eraseLocal || starArg then Term.isLocalIdent? arg[1] else pure none
if let some fvar := fvar? then
-- We use `eraseCore` because the simp theorem for the hypothesis was not added yet
thms := thms.eraseCore (.fvar fvar.fvarId!)
else
let id := arg[1]
if let .ok declName observing (realizeGlobalConstNoOverloadWithInfo id) then
if ( Simp.isSimproc declName) then
simprocs := simprocs.erase declName
else if ctx.config.autoUnfold then
thms := thms.eraseCore (.decl declName)
else
thms withRef id <| thms.erase (.decl declName)
else
-- If `id` could not be resolved, we should check whether it is a builtin simproc.
-- before returning error.
let name := id.getId.eraseMacroScopes
if ( Simp.isBuiltinSimproc name) then
simprocs := simprocs.erase name
else
withRef id <| throwUnknownConstant name
else if arg.getKind == ``Lean.Parser.Tactic.simpLemma then
let post :=
if arg[0].isNone then
true
else
arg[0][0].getKind == ``Parser.Tactic.simpPost
let inv := !arg[1].isNone
let term := arg[2]
match ( resolveSimpIdTheorem? term) with
| .expr e =>
let name mkFreshId
thms addDeclToUnfoldOrTheorem thms (.stx name arg) e post inv kind
| .simproc declName =>
simprocs simprocs.add declName post
| .ext (some ext₁) (some ext₂) _ =>
thmsArray := thmsArray.push ( ext₁.getTheorems)
simprocs := simprocs.push ( ext₂.getSimprocs)
| .ext (some ext₁) none _ =>
thmsArray := thmsArray.push ( ext₁.getTheorems)
| .ext none (some ext₂) _ =>
simprocs := simprocs.push ( ext₂.getSimprocs)
| .none =>
let name mkFreshId
thms addSimpTheorem thms (.stx name arg) term post inv
else if arg.getKind == ``Lean.Parser.Tactic.simpStar then
starArg := true
else
let id := arg[1]
if let .ok declName observing (realizeGlobalConstNoOverloadWithInfo id) then
if ( Simp.isSimproc declName) then
simprocs := simprocs.erase declName
else if ctx.config.autoUnfold then
thms := thms.eraseCore (.decl declName)
else
thms thms.erase (.decl declName)
else
-- If `id` could not be resolved, we should check whether it is a builtin simproc.
-- before returning error.
let name := id.getId.eraseMacroScopes
if ( Simp.isBuiltinSimproc name) then
simprocs := simprocs.erase name
else
throwUnknownConstant name
else if arg.getKind == ``Lean.Parser.Tactic.simpLemma then
let post :=
if arg[0].isNone then
true
else
arg[0][0].getKind == ``Parser.Tactic.simpPost
let inv := !arg[1].isNone
let term := arg[2]
match ( resolveSimpIdTheorem? term) with
| .expr e =>
let name mkFreshId
thms addDeclToUnfoldOrTheorem thms (.stx name arg) e post inv kind
| .simproc declName =>
simprocs simprocs.add declName post
| .ext (some ext₁) (some ext₂) _ =>
thmsArray := thmsArray.push ( ext₁.getTheorems)
simprocs := simprocs.push ( ext₂.getSimprocs)
| .ext (some ext₁) none _ =>
thmsArray := thmsArray.push ( ext₁.getTheorems)
| .ext none (some ext₂) _ =>
simprocs := simprocs.push ( ext₂.getSimprocs)
| .none =>
let name mkFreshId
thms addSimpTheorem thms (.stx name arg) term post inv
else if arg.getKind == ``Lean.Parser.Tactic.simpStar then
starArg := true
else
throwUnsupportedSyntax
throwUnsupportedSyntax
catch ex => logException ex
return { ctx := { ctx with simpTheorems := thmsArray.set! 0 thms }, simprocs, starArg }
where
isSimproc? (e : Expr) : MetaM (Option Name) := do
@@ -336,7 +339,9 @@ def mkSimpOnly (stx : Syntax) (usedSimps : Simp.UsedSimps) : MetaM Syntax := do
for (thm, _) in usedSimps.toArray.qsort (·.2 < ·.2) do
match thm with
| .decl declName post inv => -- global definitions in the environment
if env.contains declName && (inv || !simpOnlyBuiltins.contains declName) then
if env.contains declName
&& (inv || !simpOnlyBuiltins.contains declName)
&& !Match.isMatchEqnTheorem env declName then
let decl : Term `($(mkIdent ( unresolveNameGlobal declName)):ident)
let arg match post, inv with
| true, true => `(Parser.Tactic.simpLemma| $decl:term)

View File

@@ -13,6 +13,7 @@ import Lean.Elab.Config
import Lean.Elab.Level
import Lean.Elab.DeclModifiers
import Lean.Elab.PreDefinition.WF.TerminationHint
import Lean.Language.Basic
namespace Lean.Elab
@@ -112,6 +113,14 @@ structure State where
letRecsToLift : List LetRecToLift := []
deriving Inhabited
/--
Backtrackable state for the `TermElabM` monad.
-/
structure SavedState where
meta : Meta.SavedState
«elab» : State
deriving Nonempty
end Term
namespace Tactic
@@ -152,6 +161,42 @@ structure Cache where
post : PHashMap CacheKey Snapshot := {}
deriving Inhabited
section Snapshot
open Language
structure SavedState where
term : Term.SavedState
tactic : State
/-- State after finishing execution of a tactic. -/
structure TacticFinished where
/-- Reusable state, if no fatal exception occurred. -/
state? : Option SavedState
deriving Inhabited
/-- Snapshot just before execution of a tactic. -/
structure TacticParsedSnapshotData extends Language.Snapshot where
/-- Syntax tree of the tactic, stored and compared for incremental reuse. -/
stx : Syntax
/-- Task for state after tactic execution. -/
finished : Task TacticFinished
deriving Inhabited
/-- State after execution of a single synchronous tactic step. -/
inductive TacticParsedSnapshot where
| mk (data : TacticParsedSnapshotData) (next : Array (SnapshotTask TacticParsedSnapshot))
deriving Inhabited
abbrev TacticParsedSnapshot.data : TacticParsedSnapshot TacticParsedSnapshotData
| .mk data _ => data
/-- Potential, potentially parallel, follow-up tactic executions. -/
-- In the first, non-parallel version, each task will depend on its predecessor
abbrev TacticParsedSnapshot.next : TacticParsedSnapshot Array (SnapshotTask TacticParsedSnapshot)
| .mk _ next => next
partial instance : ToSnapshotTree TacticParsedSnapshot where
toSnapshotTree := go where
go := fun s, next => s.toSnapshot, next.map (·.map (sync := true) go)
end Snapshot
end Tactic
namespace Term
@@ -211,6 +256,13 @@ structure Context where
/-- Cache for the `save` tactic. It is only `some` in the LSP server. -/
tacticCache? : Option (IO.Ref Tactic.Cache) := none
/--
Snapshot for incremental processing of current tactic, if any.
Invariant: if the bundle's `old?` is set, then the state *up to the start* of the tactic is
unchanged, i.e. reuse is possible.
-/
tacSnap? : Option (Language.SnapshotBundle Tactic.TacticParsedSnapshot) := none
/--
If `true`, we store in the `Expr` the `Syntax` for recursive applications (i.e., applications
of free variables tagged with `isAuxDecl`). We store the `Syntax` using `mkRecAppWithSyntax`.
We use the `Syntax` object to produce better error messages at `Structural.lean` and `WF.lean`. -/
@@ -241,14 +293,6 @@ open Meta
instance : Inhabited (TermElabM α) where
default := throw default
/--
Backtrackable state for the `TermElabM` monad.
-/
structure SavedState where
meta : Meta.SavedState
«elab» : State
deriving Nonempty
protected def saveState : TermElabM SavedState :=
return { meta := ( Meta.saveState), «elab» := ( get) }
@@ -261,18 +305,87 @@ def SavedState.restore (s : SavedState) (restoreInfo : Bool := false) : TermElab
unless restoreInfo do
setInfoState infoState
/--
Restores full state including sources for unique identifiers. Only intended for incremental reuse
between elaboration runs, not for backtracking within a single run.
-/
def SavedState.restoreFull (s : SavedState) : TermElabM Unit := do
s.meta.restoreFull
set s.elab
@[specialize, inherit_doc Core.withRestoreOrSaveFull]
def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState))
(cont : TermElabM SavedState TermElabM α) : TermElabM α := do
if let some (_, state) := reusableResult? then
set state.elab
let reusableResult? := reusableResult?.map (fun (val, state) => (val, state.meta))
controlAt MetaM fun runInBase =>
Meta.withRestoreOrSaveFull reusableResult? fun restore =>
runInBase <| cont (return { meta := ( restore), «elab» := ( get) })
instance : MonadBacktrack SavedState TermElabM where
saveState := Term.saveState
restoreState b := b.restore
/--
Manages reuse information for nested tactics by `split`ting given syntax into an outer and inner
part. `act` is then run on the inner part but with reuse information adjusted as following:
* If the old (from `tacSnap?`'s `SyntaxGuarded.stx`) and new (from `stx`) outer syntax are not
identical according to `Syntax.structRangeEq`, reuse is disabled.
* Otherwise, the old syntax as stored in `tacSnap?` is updated to the old *inner* syntax.
* In any case, we also use `withRef` on the inner syntax to avoid leakage of the outer syntax into
`act` via this route.
For any tactic that participates in reuse, `withNarrowedTacticReuse` should be applied to the
tactic's syntax and `act` should be used to do recursive tactic evaluation of nested parts.
-/
def withNarrowedTacticReuse [Monad m] [MonadExceptOf Exception m] [MonadWithReaderOf Context m]
[MonadOptions m] [MonadRef m] (split : Syntax Syntax × Syntax) (act : Syntax m α)
(stx : Syntax) : m α := do
let (outer, inner) := split stx
let opts getOptions
withTheReader Term.Context (fun ctx => { ctx with tacSnap? := ctx.tacSnap?.map fun tacSnap =>
{ tacSnap with old? := tacSnap.old?.bind fun old => do
let (oldOuter, oldInner) := split old.stx
guard <| outer.structRangeEqWithTraceReuse opts oldOuter
return { old with stx := oldInner }
}
}) do
withRef inner do
act inner
/--
A variant of `withNarrowedTacticReuse` that uses `stx[argIdx]` as the inner syntax and all `stx`
child nodes before that as the outer syntax, i.e. reuse is disabled if there was any change before
`argIdx`.
NOTE: child nodes after `argIdx` are not tested (which would almost always disable reuse as they are
necessarily shifted by changes at `argIdx`) so it must be ensured that the result of `arg` does not
depend on them (i.e. they should not be inspected beforehand).
-/
def withNarrowedArgTacticReuse [Monad m] [MonadExceptOf Exception m] [MonadWithReaderOf Context m]
[MonadOptions m] [MonadRef m] (argIdx : Nat) (act : Syntax m α) (stx : Syntax) : m α :=
withNarrowedTacticReuse (fun stx => (mkNullNode stx.getArgs[:argIdx], stx[argIdx])) act stx
/--
Disables incremental tactic reuse *and* reporting for `act` if `cond` is true by setting `tacSnap?`
to `none`. This should be done for tactic blocks that are run multiple times as otherwise the
reported progress will jump back and forth (and partial reuse for these kinds of tact blocks is
similarly questionable).
-/
def withoutTacticIncrementality [Monad m] [MonadWithReaderOf Context m] [MonadOptions m] [MonadRef m]
(cond : Bool) (act : m α) : m α := do
let opts getOptions
withTheReader Term.Context (fun ctx => { ctx with tacSnap? := ctx.tacSnap?.filter fun tacSnap => Id.run do
if let some old := tacSnap.old? then
if cond && opts.getBool `trace.Elab.reuse then
dbg_trace "reuse stopped: guard failed at {old.stx}"
return !cond
}) act
/-- Disables incremental tactic reuse for `act` if `cond` is true. -/
def withoutTacticReuse [Monad m] [MonadWithReaderOf Context m] [MonadOptions m] [MonadRef m]
(cond : Bool) (act : m α) : m α := do
let opts getOptions
withTheReader Term.Context (fun ctx => { ctx with tacSnap? := ctx.tacSnap?.map fun tacSnap =>
{ tacSnap with old? := tacSnap.old?.filter fun old => Id.run do
if cond && opts.getBool `trace.Elab.reuse then
dbg_trace "reuse stopped: guard failed at {old.stx}"
return !cond }
}) act
abbrev TermElabResult (α : Type) := EStateM.Result Exception SavedState α
/--
@@ -1531,7 +1644,7 @@ partial def withAutoBoundImplicit (k : TermElabM α) : TermElabM α := do
| ex => match isAutoBoundImplicitLocalException? ex with
| some n =>
-- Restore state, declare `n`, and try again
s.restore
s.restore (restoreInfo := true)
withLocalDecl n .implicit ( mkFreshTypeMVar) fun x =>
withReader (fun ctx => { ctx with autoBoundImplicits := ctx.autoBoundImplicits.push x } ) do
loop ( saveState)
@@ -1769,6 +1882,33 @@ builtin_initialize
registerTraceClass `Elab.debug
registerTraceClass `Elab.reuse
builtin_initialize incrementalAttr : TagAttribute
registerTagAttribute `incremental "Marks an elaborator (tactic or command, currently) as \
supporting incremental elaboration. For unmarked elaborators, the corresponding snapshot bundle \
field in the elaboration context is unset so as to prevent accidental, incorrect reuse."
builtin_initialize builtinIncrementalElabs : IO.Ref NameSet IO.mkRef {}
def addBuiltinIncrementalElab (decl : Name) : IO Unit := do
builtinIncrementalElabs.modify fun s => s.insert decl
builtin_initialize
registerBuiltinAttribute {
name := `builtin_incremental
descr := s!"(builtin) {incrementalAttr.attr.descr}"
applicationTime := .afterCompilation
add := fun decl stx kind => do
Attribute.Builtin.ensureNoArgs stx
unless kind == AttributeKind.global do
throwError "invalid attribute 'builtin_incremental', must be global"
declareBuiltin decl <| mkApp (mkConst ``addBuiltinIncrementalElab) (toExpr decl)
}
/-- Checks whether a declaration is annotated with `[builtin_incremental]` or `[incremental]`. -/
def isIncrementalElab [Monad m] [MonadEnv m] [MonadLiftT IO m] (decl : Name) : m Bool :=
(return ( builtinIncrementalElabs.get (m := IO)).contains decl) <||>
(return incrementalAttr.hasTag ( getEnv) decl)
export Term (TermElabM)
end Lean.Elab

View File

@@ -205,10 +205,13 @@ def logException [Monad m] [MonadLog m] [AddMessageContext m] [MonadOptions m] [
match ex with
| Exception.error ref msg => logErrorAt ref msg
| Exception.internal id _ =>
unless isAbortExceptionId id do
unless isAbortExceptionId id || id == Core.interruptExceptionId do
let name id.getName
logError m!"internal exception: {name}"
/--
If `x` throws an exception, catch it and turn it into a log message (using `logException`).
-/
def withLogging [Monad m] [MonadLog m] [MonadExcept Exception m] [AddMessageContext m] [MonadOptions m] [MonadLiftT IO m]
(x : m Unit) : m Unit := do
try x catch ex => logException ex

View File

@@ -17,8 +17,13 @@ set_option linter.missingDocs true
namespace Lean.Language
/-- `MessageLog` with interactive diagnostics. -/
/--
`MessageLog` with interactive diagnostics.
Can be created using `Diagnostics.empty` or `Diagnostics.ofMessageLog`.
-/
structure Snapshot.Diagnostics where
private mk ::
/-- Non-interactive message log. -/
msgLog : MessageLog
/--
@@ -133,8 +138,7 @@ checking if we can reuse `old?` if set or else redoing the corresponding elabora
case, we derive new bundles for nested snapshots, if any, and finally `resolve` `new` to the result.
Note that failing to `resolve` a created promise will block the language server indefinitely!
Corresponding `IO.Promise.new` calls should come with a "definitely resolved in ..." comment
explaining how this is avoided in each case.
We use `withAlwaysResolvedPromise`/`withAlwaysResolvedPromises` to ensure this doesn't happen.
In the future, the 1-element history `old?` may be replaced with a global cache indexed by strong
hashes but the promise will still need to be passed through the elaborator.
@@ -151,6 +155,36 @@ structure SnapshotBundle (α : Type) where
-/
new : IO.Promise α
/--
Runs `act` with a newly created promise and finally resolves it to `default` if not done by `act`.
Always resolving promises involved in the snapshot tree is important to avoid deadlocking the
language server.
-/
def withAlwaysResolvedPromise [Monad m] [MonadLiftT BaseIO m] [MonadFinally m] [Inhabited α]
(act : IO.Promise α m Unit) : m Unit := do
let p IO.Promise.new
try
act p
finally
p.resolve default
/--
Runs `act` with `count` newly created promises and finally resolves them to `default` if not done by
`act`.
Always resolving promises involved in the snapshot tree is important to avoid deadlocking the
language server.
-/
def withAlwaysResolvedPromises [Monad m] [MonadLiftT BaseIO m] [MonadFinally m] [Inhabited α]
(count : Nat) (act : Array (IO.Promise α) m Unit) : m Unit := do
let ps List.iota count |>.toArray.mapM fun _ => IO.Promise.new
try
act ps
finally
for p in ps do
p.resolve default
/--
Tree of snapshots where each snapshot comes with an array of asynchronous further subtrees. Used
for asynchronously collecting information about the entirety of snapshots in the language server.

View File

@@ -63,36 +63,61 @@ we remain at "go two commands up" at this point.
Because of Lean's use of persistent data structures, incremental reuse of fully elaborated commands
is easy because we can simply snapshot the entire state after each command and then restart
elaboration using the stored state at the point of change. However, incrementality within
elaboration of a single command such as between tactic steps is much harder because we cannot simply
return from those points to the language processor in a way that we can later resume from there.
Instead, we exchange the need for continuations with some limited mutability: by allocating an
`IO.Promise` "cell" in the language processor, we can both pass it to the elaborator to eventually
fill it using `Promise.resolve` as well as convert it to a `Task` that will wait on that resolution
using `Promise.result` and return it as part of the command snapshot created by the language
processor. The elaborator can then create new promises itself and store their `result` when
resolving an outer promise to create an arbitrary tree of promise-backed snapshot tasks. Thus, we
can enable incremental reporting and reuse inside the elaborator using the same snapshot tree data
structures as outside without having to change the elaborator's control flow.
elaboration using the stored state at the next command above the point of change. However,
incrementality *within* elaboration of a single command such as between tactic steps is much harder
because the existing control flow does not allow us to simply return from those points to the
language processor in a way that we can later resume from there. Instead, we exchange the need for
continuations with some limited mutability: by allocating an `IO.Promise` "cell" in the language
processor, we can both pass it to the elaborator to eventually fill it using `Promise.resolve` as
well as convert it to a `Task` that will wait on that resolution using `Promise.result` and return
it as part of the command snapshot created by the language processor. The elaborator can then in
turn create new promises itself and store their `result` when resolving an outer promise to create
an arbitrary tree of promise-backed snapshot tasks. Thus, we can enable incremental reporting and
reuse inside the elaborator using the same snapshot tree data structures as outside without having
to change the elaborator's control flow.
While ideally we would decide what can be reused during command elaboration using strong hashes over
the state and inputs, currently we rely on simpler syntactic checks: if all the syntax inspected up
to a certain point is unchanged, we can assume that the old state can be reused. The central
`SnapshotBundle` type passed inwards through the elaborator for this purpose combines the following
data:
the full state and inputs, currently we rely on simpler syntactic checks: if all the syntax
inspected up to a certain point is unchanged, we can assume that the old state can be reused. The
central `SnapshotBundle` type passed inwards through the elaborator for this purpose combines the
following data:
* the `IO.Promise` to be resolved to an elaborator snapshot (whose type depends on the specific
elaborator part we're in, e.g. `)
elaborator part we're in, e.g. `TacticParsedSnapshot`, `BodyProcessedSnapshot`)
* if there was a previous run:
* a `SnapshotTask` holding the corresponding snapshot of the run
* the relevant `Syntax` of the previous run to be compared before any reuse
Note that as we do not wait for the previous run to finish before starting to elaborate the next
one, the `SnapshotTask` task may not be finished yet. Indeed, if we do find that we can reuse the
contained state, we will want to explicitly wait for it instead of redoing the work. On the other
hand, the `Syntax` is not surrounded by a task so that we can immediately access it for comparisons,
even if the snapshot task may, eventually, give access to the same syntax tree.
one, the old `SnapshotTask` task may not be finished yet. Indeed, if we do find that we can reuse
the contained state because of a successful syntax comparison, we always want to explicitly wait for
the task instead of redoing the work. On the other hand, the `Syntax` is not surrounded by a task so
that we can immediately access it for comparisons, even if the snapshot task may, eventually, give
access to the same syntax tree.
TODO: tactic examples
For the most part, inside an elaborator participating in incrementality, we just have to ensure that
we stop forwarding the old run's data as soon as we notice a relevant difference between old and new
syntax tree. For example, allowing incrementality inside the cdot tactic combinator is as simple as
```
builtin_initialize registerBuiltinIncrementalTactic ``cdot
@[builtin_tactic cdot] def evalTacticCDot : Tactic := fun stx => do
...
closeUsingOrAdmit do
-- save state before/after entering focus on `·`
...
Term.withNarrowedArgTacticReuse (argIdx := 1) evalTactic stx
```
The `Term.withNarrowedArgTacticReuse` combinator will focus on the given argument of `stx`, which in
this case is the nested tactic sequence, and run `evalTactic` on it. But crucially, it will first
compare all preceding arguments, in this case the cdot token itself, with the old syntax in the
current snapshot bundle, which in the case of tactics is stored in `Term.Context.tacSnap?`. Indeed
it is important here to check if the cdot token is identical because its position has been saved in
the info tree, so it would be bad if we later restored some old state that uses a different position
for it even if everything else is unchanged. If there is any mismatch, the bundle's old value is
set to `none` in order to prevent reuse from this point on. Note that in any case we still want to
forward the "new" promise in order to provide incremental reporting as well as to construct a
snapshot tree for reuse in future document versions! Note also that we explicitly opted into
incrementality using `registerBuiltinIncrementalTactic` as any tactic combinator not written with
these concerns in mind would likely misbehave under incremental reuse.
While it is generally true that we can provide incremental reporting even without reuse, we
generally want to avoid that when it would be confusing/annoying, e.g. when a tactic block is run
@@ -101,12 +126,24 @@ purpose, we can disable both incremental modes using `Term.withoutTacticIncremen
opted into incrementality because of other parts of the combinator. `induction` is an example of
this because there are some induction alternatives that are run multiple times, so we disable all of
incrementality for them.
Using `induction` as a more complex example than `cdot` as it calls into `evalTactic` multiple
times, here is a summary of what it has to do to implement incrementality:
* `Narrow` down to the syntax of alternatives, disabling reuse if anything before them changed
* allocate one new promise for each given alternative, immediately resolve passed promise to a new
snapshot tree node holding them so that the language server can wait on them
* when executing an alternative,
* we put the corresponding promise into the context
* we disable reuse if anything in front of the contained tactic block has changed, including
previous alternatives
* we disable reuse *and reporting* if the tactic block is run multiple times, e.g. in the case of
a wildcard pattern
-/
set_option linter.missingDocs true
namespace Lean.Language.Lean
open Lean.Elab
open Lean.Elab Command
open Lean.Parser
private def pushOpt (a? : Option α) (as : Array α) : Array α :=
@@ -121,12 +158,6 @@ register_builtin_option stderrAsMessages : Bool := {
descr := "(server) capture output to the Lean stderr channel (such as from `dbg_trace`) during elaboration of a command as a diagnostic message"
}
/-- Option for showing elaboration errors from partial syntax errors. -/
register_builtin_option showPartialSyntaxErrors : Bool := {
defValue := false
descr := "show elaboration errors from partial syntax trees (i.e. after parser recovery)"
}
/-! The hierarchy of Lean snapshot types -/
/-- Snapshot after elaboration of the entire command. -/
@@ -165,7 +196,7 @@ deriving Nonempty
abbrev CommandParsedSnapshot.data : CommandParsedSnapshot CommandParsedSnapshotData
| mk data _ => data
/-- Next command, unless this is a terminal command. -/
abbrev CommandParsedSnapshot.next? : CommandParsedSnapshot
abbrev CommandParsedSnapshot.nextCmdSnap? : CommandParsedSnapshot
Option (SnapshotTask CommandParsedSnapshot)
| mk _ next? => next?
partial instance : ToSnapshotTree CommandParsedSnapshot where
@@ -173,18 +204,7 @@ partial instance : ToSnapshotTree CommandParsedSnapshot where
go s := s.data.toSnapshot,
#[s.data.elabSnap.map (sync := true) toSnapshotTree,
s.data.finishedSnap.map (sync := true) toSnapshotTree] |>
pushOpt (s.next?.map (·.map (sync := true) go))
/-- Cancels all significant computations from this snapshot onwards. -/
partial def CommandParsedSnapshot.cancel (snap : CommandParsedSnapshot) : BaseIO Unit := do
-- This is the only relevant computation right now, everything else is promises
-- TODO: cancel additional elaboration tasks (which will be tricky with `DynamicSnapshot`) if we
-- add them without switching to implicit cancellation
snap.data.finishedSnap.cancel
if let some next := snap.next? then
-- recurse on next command (which may have been spawned just before we cancelled above)
let _ IO.mapTask (sync := true) (·.cancel) next.task
pushOpt (s.nextCmdSnap?.map (·.map (sync := true) go))
/-- State after successful importing. -/
structure HeaderProcessedState where
@@ -218,6 +238,8 @@ structure HeaderParsedSnapshot extends Snapshot where
/-- State after successful parsing. -/
result? : Option HeaderParsedState
isFatal := result?.isNone
/-- Cancellation token for interrupting processing of this run. -/
cancelTk? : Option IO.CancelToken
instance : ToSnapshotTree HeaderParsedSnapshot where
toSnapshotTree s := s.toSnapshot,
@@ -235,6 +257,10 @@ abbrev InitialSnapshot := HeaderParsedSnapshot
structure LeanProcessingContext extends ProcessingContext where
/-- Position of the first file difference if there was a previous invocation. -/
firstDiffPos? : Option String.Pos
/-- Cancellation token of the previous invocation, if any. -/
oldCancelTk? : Option IO.CancelToken
/-- Cancellation token of the current run. -/
newCancelTk : IO.CancelToken
/-- Monad transformer holding all relevant data for Lean processing. -/
abbrev LeanProcessingT m := ReaderT LeanProcessingContext m
@@ -247,6 +273,18 @@ instance : MonadLift LeanProcessingM (LeanProcessingT IO) where
instance : MonadLift (ProcessingT m) (LeanProcessingT m) where
monadLift := fun act ctx => act ctx.toProcessingContext
/--
Embeds a `LeanProcessingM` action into `ProcessingM`, optionally using the old input string to speed
up reuse analysis and supplying a cancellation token that should be triggered as soon as reuse is
ruled out.
-/
def LeanProcessingM.run (act : LeanProcessingM α) (oldInputCtx? : Option InputContext)
(oldCancelTk? : Option IO.CancelToken := none) : ProcessingM α := do
-- compute position of syntactic change once
let firstDiffPos? := oldInputCtx?.map (·.input.firstDiffPos ( read).input)
let newCancelTk IO.CancelToken.new
ReaderT.adapt ({ · with firstDiffPos?, oldCancelTk?, newCancelTk }) act
/--
Returns true if there was a previous run and the given position is before any textual change
compared to it.
@@ -291,8 +329,7 @@ General notes:
state. As there is no cheap way to check whether the `Environment` is unchanged, i.e. *semantic*
change detection is currently not possible, we must make sure to pass `none` as all follow-up
"previous states" from the first *syntactic* change onwards.
* We must make sure to use `CommandParsedSnapshot.cancel` on such tasks when discarding them, i.e.
when not passing them along in `old?`.
* We must make sure to trigger `oldCancelTk?` as soon as discarding `old?`.
* Control flow up to finding the last still-valid snapshot (which should be quick) is synchronous so
as not to report this "fast forwarding" to the user as well as to make sure the next run sees all
fast-forwarded snapshots without having to wait on tasks.
@@ -300,40 +337,47 @@ General notes:
partial def process
(setupImports : Syntax ProcessingT IO (Except HeaderProcessedSnapshot SetupImportsResult))
(old? : Option InitialSnapshot) : ProcessingM InitialSnapshot := do
-- compute position of syntactic change once
let firstDiffPos? := old?.map (·.ictx.input.firstDiffPos ( read).input)
ReaderT.adapt ({ · with firstDiffPos? }) do
parseHeader old?
parseHeader old? |>.run (old?.map (·.ictx)) (old?.bind (·.cancelTk?))
where
parseHeader (old? : Option HeaderParsedSnapshot) : LeanProcessingM HeaderParsedSnapshot := do
let ctx read
let ictx := ctx.toInputContext
let unchanged old :=
let unchanged old newParserState :=
-- when header syntax is unchanged, reuse import processing task as is and continue with
-- parsing the first command, synchronously if possible
-- NOTE: even if the syntax tree is functionally unchanged, the new parser state may still
-- have changed because of trailing whitespace and comments etc., so it is passed separately
-- from `old`
if let some oldSuccess := old.result? then
return { old with ictx, result? := some { oldSuccess with
processedSnap := ( oldSuccess.processedSnap.bindIO (sync := true) fun oldProcessed => do
if let some oldProcSuccess := oldProcessed.result? then
-- also wait on old command parse snapshot as parsing is cheap and may allow for
-- elaboration reuse
oldProcSuccess.firstCmdSnap.bindIO (sync := true) fun oldCmd =>
return .pure { oldProcessed with result? := some { oldProcSuccess with
firstCmdSnap := ( parseCmd oldCmd oldSuccess.parserState oldProcSuccess.cmdState ctx) } }
else
return .pure oldProcessed) } }
return {
ictx
stx := old.stx
diagnostics := old.diagnostics
cancelTk? := ctx.newCancelTk
result? := some { oldSuccess with
processedSnap := ( oldSuccess.processedSnap.bindIO (sync := true) fun oldProcessed => do
if let some oldProcSuccess := oldProcessed.result? then
-- also wait on old command parse snapshot as parsing is cheap and may allow for
-- elaboration reuse
oldProcSuccess.firstCmdSnap.bindIO (sync := true) fun oldCmd =>
return .pure { oldProcessed with result? := some { oldProcSuccess with
firstCmdSnap := ( parseCmd oldCmd newParserState oldProcSuccess.cmdState ctx) } }
else
return .pure oldProcessed) } }
else return old
-- fast path: if we have parsed the header successfully...
if let some old := old? then
if let some (some processed) old.processedResult.get? then
-- ...and the edit location is after the next command (see note [Incremental Parsing])...
if let some nextCom processed.firstCmdSnap.get? then
if ( isBeforeEditPos nextCom.data.parserState.pos) then
-- ...go immediately to next snapshot
return ( unchanged old)
if let some oldSuccess := old.result? then
if let some (some processed) old.processedResult.get? then
-- ...and the edit location is after the next command (see note [Incremental Parsing])...
if let some nextCom processed.firstCmdSnap.get? then
if ( isBeforeEditPos nextCom.data.parserState.pos) then
-- ...go immediately to next snapshot
return ( unchanged old oldSuccess.parserState)
withHeaderExceptions ({ · with ictx, stx := .missing, result? := none }) do
withHeaderExceptions ({ · with
ictx, stx := .missing, result? := none, cancelTk? := none }) do
-- parsing the header should be cheap enough to do synchronously
let (stx, parserState, msgLog) Parser.parseHeader ictx
if msgLog.hasErrors then
@@ -341,6 +385,7 @@ where
ictx, stx
diagnostics := ( Snapshot.Diagnostics.ofMessageLog msgLog)
result? := none
cancelTk? := none
}
-- semi-fast path: go to next snapshot if syntax tree is unchanged AND we're still in front
@@ -351,14 +396,11 @@ where
-- influence the range of error messages such as from a trailing `exact`
if let some old := old? then
if ( isBeforeEditPos parserState.pos) && old.stx == stx then
return ( unchanged old)
-- on first change, make sure to cancel all further old tasks
if let some oldSuccess := old.result? then
oldSuccess.processedSnap.cancel
let _ BaseIO.mapTask (t := oldSuccess.processedSnap.task) fun processed => do
if let some oldProcSuccess := processed.result? then
let _ BaseIO.mapTask (·.cancel) oldProcSuccess.firstCmdSnap.task
-- Here we must make sure to pass the *new* parser state; see NOTE in `unchanged`
return ( unchanged old parserState)
-- on first change, make sure to cancel old invocation
if let some tk := ctx.oldCancelTk? then
tk.set
return {
ictx, stx
diagnostics := ( Snapshot.Diagnostics.ofMessageLog msgLog)
@@ -366,6 +408,7 @@ where
parserState
processedSnap := ( processHeader stx parserState)
}
cancelTk? := ctx.newCancelTk
}
processHeader (stx : Syntax) (parserState : Parser.ModuleParserState) :
@@ -417,7 +460,7 @@ where
-- check for cancellation, most likely during elaboration of previous command, before starting
-- processing of next command
if ( IO.checkCanceled) then
if ( ctx.newCancelTk.isSet) then
-- this is a bit ugly as we don't want to adjust our API with `Option`s just for cancellation
-- (as no-one should look at this result in that case) but anything containing `Environment`
-- is not `Inhabited`
@@ -428,22 +471,25 @@ where
tacticCache := ( IO.mkRef {})
}
let unchanged old : BaseIO CommandParsedSnapshot :=
let unchanged old newParserState : BaseIO CommandParsedSnapshot :=
-- when syntax is unchanged, reuse command processing task as is
if let some oldNext := old.next? then
-- NOTE: even if the syntax tree is functionally unchanged, the new parser state may still
-- have changed because of trailing whitespace and comments etc., so it is passed separately
-- from `old`
if let some oldNext := old.nextCmdSnap? then
return .mk (data := old.data)
(nextCmdSnap? := ( old.data.finishedSnap.bindIO (sync := true) fun oldFinished =>
-- also wait on old command parse snapshot as parsing is cheap and may allow for
-- elaboration reuse
oldNext.bindIO (sync := true) fun oldNext => do
parseCmd oldNext old.data.parserState oldFinished.cmdState ctx))
-- also wait on old command parse snapshot as parsing is cheap and may allow for
-- elaboration reuse
oldNext.bindIO (sync := true) fun oldNext => do
parseCmd oldNext newParserState oldFinished.cmdState ctx))
else return old -- terminal command, we're done!
-- fast path, do not even start new task for this snapshot
if let some old := old? then
if let some nextCom old.next?.bindM (·.get?) then
if let some nextCom old.nextCmdSnap?.bindM (·.get?) then
if ( isBeforeEditPos nextCom.data.parserState.pos) then
return .pure ( unchanged old)
return .pure ( unchanged old old.data.parserState)
SnapshotTask.ofIO (some parserState.pos, ctx.input.endPos) do
let beginPos := parserState.pos
@@ -458,15 +504,19 @@ where
-- semi-fast path
if let some old := old? then
if ( isBeforeEditPos parserState.pos ctx) && old.data.stx == stx then
return ( unchanged old)
-- on first change, make sure to cancel all further old tasks
old.cancel
-- Here we must make sure to pass the *new* parser state; see NOTE in `unchanged`
return ( unchanged old parserState)
-- on first change, make sure to cancel old invocation
-- TODO: pass token into incrementality-aware elaborators to improve reuse of still-valid,
-- still-running elaboration steps?
if let some tk := ctx.oldCancelTk? then
tk.set
-- definitely resolved in `doElab` task
let elabPromise IO.Promise.new
let tacticCache old?.map (·.data.tacticCache) |>.getDM (IO.mkRef {})
let finishedSnap
doElab stx cmdState msgLog.hasErrors beginPos
doElab stx cmdState beginPos
{ old? := old?.map fun old => old.data.stx, old.data.elabSnap, new := elabPromise }
tacticCache
ctx
@@ -479,19 +529,24 @@ where
diagnostics := ( Snapshot.Diagnostics.ofMessageLog msgLog)
stx
parserState
elabSnap := { range? := finishedSnap.range?, task := elabPromise.result }
elabSnap := { range? := stx.getRange?, task := elabPromise.result }
finishedSnap
tacticCache
}
doElab (stx : Syntax) (cmdState : Command.State) (hasParseError : Bool) (beginPos : String.Pos)
doElab (stx : Syntax) (cmdState : Command.State) (beginPos : String.Pos)
(snap : SnapshotBundle DynamicSnapshot) (tacticCache : IO.Ref Tactic.Cache) :
LeanProcessingM (SnapshotTask CommandFinishedSnapshot) := do
let ctx read
-- signature elaboration task; for now, does full elaboration
-- TODO: do tactic snapshots, reuse old state for them
SnapshotTask.ofIO (stx.getRange?.getD beginPos, beginPos) do
-- (Try to) use last line of command as range for final snapshot task. This ensures we do not
-- retract the progress bar to a previous position in case the command support incremental
-- reporting but has significant work after resolving its last incremental promise, such as
-- final type checking; if it does not support incrementality, `elabSnap` constructed in
-- `parseCmd` and containing the entire range of the command will determine the reported
-- progress and be resolved effectively at the same time as this snapshot task, so `tailPos` is
-- irrelevant in this case.
let tailPos := stx.getTailPos? |>.getD beginPos
SnapshotTask.ofIO (some tailPos, tailPos) do
let scope := cmdState.scopes.head!
let cmdStateRef IO.mkRef { cmdState with messages := .empty }
/-
@@ -505,26 +560,18 @@ where
cmdPos := beginPos
tacticCache? := some tacticCacheNew
snap? := some snap
cancelTk? := some ctx.newCancelTk
}
let (output, _)
IO.FS.withIsolatedStreams (isolateStderr := stderrAsMessages.get scope.opts) do
liftM (m := BaseIO) do
Elab.Command.catchExceptions
withLoggingExceptions
(getResetInfoTrees *> Elab.Command.elabCommandTopLevel stx)
cmdCtx cmdStateRef
let postNew := ( tacticCacheNew.get).post
tacticCache.modify fun _ => { pre := postNew, post := {} }
let cmdState cmdStateRef.get
let mut messages := cmdState.messages
-- `stx.hasMissing` should imply `hasParseError`, but the latter should be cheaper to check in
-- general
if !showPartialSyntaxErrors.get cmdState.scopes[0]!.opts && hasParseError &&
stx.hasMissing then
-- discard elaboration errors, except for a few important and unlikely misleading ones, on
-- parse error
messages := messages.msgs.filter fun msg =>
msg.data.hasTag (fun tag => tag == `Elab.synthPlaceholder ||
tag == `Tactic.unsolvedGoals || (`_traceMsg).isSuffixOf tag)
if !output.isEmpty then
messages := messages.add {
fileName := ctx.fileName
@@ -541,13 +588,25 @@ where
cmdState
}
/--
Convenience function for tool uses of the language processor that skips header handling.
-/
def processCommands (inputCtx : Parser.InputContext) (parserState : Parser.ModuleParserState)
(commandState : Command.State)
(old? : Option (Parser.InputContext × CommandParsedSnapshot) := none) :
BaseIO (SnapshotTask CommandParsedSnapshot) := do
process.parseCmd (old?.map (·.2)) parserState commandState
|>.run (old?.map (·.1))
|>.run { inputCtx with }
/-- Waits for and returns final environment, if importing was successful. -/
partial def waitForFinalEnv? (snap : InitialSnapshot) : Option Environment := do
let snap snap.result?
let snap snap.processedSnap.get.result?
goCmd snap.firstCmdSnap.get
where goCmd snap :=
if let some next := snap.next? then
if let some next := snap.nextCmdSnap? then
goCmd next.get
else
snap.data.finishedSnap.get.cmdState.env

View File

@@ -57,27 +57,37 @@ inductive MessageData where
This constructor is inspected in various hacks. -/
| ofFormatWithInfos : FormatWithInfos MessageData
| ofGoal : MVarId MessageData
/-- A widget instance.
In `ofWidget wi alt`,
the nested message `alt` should approximate the contents of the widget
without itself using `ofWidget wi _`.
This is used as fallback in environments that cannot display user widgets.
`alt` may nest any structured message,
for example `ofGoal` to approximate a tactic state widget,
and, if necessary, even other widget instances
(for which approximations are computed recursively). -/
| ofWidget : Widget.WidgetInstance MessageData MessageData
/-- `withContext ctx d` specifies the pretty printing context `(env, mctx, lctx, opts)` for the nested expressions in `d`. -/
| withContext : MessageDataContext MessageData MessageData
| withNamingContext : NamingContext MessageData MessageData
/-- Lifted `Format.nest` -/
| nest : Nat MessageData MessageData
| nest : Nat MessageData MessageData
/-- Lifted `Format.group` -/
| group : MessageData MessageData
| group : MessageData MessageData
/-- Lifted `Format.compose` -/
| compose : MessageData MessageData MessageData
| compose : MessageData MessageData MessageData
/-- Tagged sections. `Name` should be viewed as a "kind", and is used by `MessageData` inspector functions.
Example: an inspector that tries to find "definitional equality failures" may look for the tag "DefEqFailure". -/
| tagged : Name MessageData MessageData
| trace (data : TraceData) (msg : MessageData) (children : Array MessageData)
/-- A lazy message.
The provided thunk will not be run until it is about to be displayed.
This can save computation in cases where the message may never be seen,
e.g. when nested inside a collapsed trace.
This can save computation in cases where the message may never be seen.
The `Dynamic` value is expected to be a `MessageData`,
which is a workaround for the positivity restriction.
If the thunked message is produced for a term that contains a synthetic sorry,
`hasSyntheticSorry` should return `true`.
This is used to filter out certain messages. -/
@@ -160,6 +170,7 @@ partial def formatAux : NamingContext → Option MessageDataContext → MessageD
| _, _, ofFormatWithInfos fmt => return fmt.1
| _, none, ofGoal mvarId => return "goal " ++ format (mkMVar mvarId)
| nCtx, some ctx, ofGoal mvarId => ppGoal (mkPPContext nCtx ctx) mvarId
| nCtx, ctx, ofWidget _ d => formatAux nCtx ctx d
| nCtx, _, withContext ctx d => formatAux nCtx ctx d
| _, ctx, withNamingContext nCtx d => formatAux nCtx ctx d
| nCtx, ctx, tagged _ d => formatAux nCtx ctx d
@@ -298,47 +309,69 @@ protected def toJson (msg : Message) : IO Json := do
end Message
/-- A persistent array of messages. -/
/--
A persistent array of messages.
In the Lean elaborator, we use a fresh message log per command but may also report diagnostics at
various points inside a command, which will empty `unreported` and updated `hadErrors` accordingly
(see `CoreM.getAndEmptyMessageLog`).
-/
structure MessageLog where
msgs : PersistentArray Message := {}
/--
If true, there was an error in the log previously that has already been reported to the user and
removed from the log. Thus we say that in the current context (usually the current command), we
"have errors" if either this flag is set or there is an error in `msgs`; see
`MessageLog.hasErrors`. If we have errors, we suppress some error messages that are often the
result of a previous error.
-/
/-
Design note: We considered introducing a `hasErrors` field instead that already includes the
presence of errors in `msgs` but this would not be compatible with e.g.
`MessageLog.errorsToWarnings`.
-/
hadErrors : Bool := false
/-- The list of messages not already reported, in insertion order. -/
unreported : PersistentArray Message := {}
deriving Inhabited
namespace MessageLog
def empty : MessageLog := {}
def empty : MessageLog := {}
def isEmpty (log : MessageLog) : Bool :=
log.msgs.isEmpty
@[deprecated "renamed to `unreported`; direct access should in general be avoided in favor of \
using `MessageLog.toList/toArray`"]
def msgs : MessageLog PersistentArray Message := unreported
def hasUnreported (log : MessageLog) : Bool :=
!log.unreported.isEmpty
def add (msg : Message) (log : MessageLog) : MessageLog :=
log.msgs.push msg
{ log with unreported := log.unreported.push msg }
protected def append (l₁ l₂ : MessageLog) : MessageLog :=
l₁.msgs ++ l₂.msgs
{ hadErrors := l₁.hadErrors || l₂.hadErrors, unreported := l₁.unreported ++ l₂.unreported }
instance : Append MessageLog :=
MessageLog.append
def hasErrors (log : MessageLog) : Bool :=
log.msgs.any fun m => match m.severity with
| MessageSeverity.error => true
| _ => false
log.hadErrors || log.unreported.any (·.severity matches .error)
def errorsToWarnings (log : MessageLog) : MessageLog :=
{ msgs := log.msgs.map (fun m => match m.severity with | MessageSeverity.error => { m with severity := MessageSeverity.warning } | _ => m) }
{ unreported := log.unreported.map (fun m => match m.severity with | MessageSeverity.error => { m with severity := MessageSeverity.warning } | _ => m) }
def getInfoMessages (log : MessageLog) : MessageLog :=
{ msgs := log.msgs.filter fun m => match m.severity with | MessageSeverity.information => true | _ => false }
{ unreported := log.unreported.filter fun m => match m.severity with | MessageSeverity.information => true | _ => false }
def forM {m : Type Type} [Monad m] (log : MessageLog) (f : Message m Unit) : m Unit :=
log.msgs.forM f
log.unreported.forM f
/-- Converts the log to a list, oldest message first. -/
/-- Converts the unreported messages to a list, oldest message first. -/
def toList (log : MessageLog) : List Message :=
log.msgs.toList
log.unreported.toList
/-- Converts the log to an array, oldest message first. -/
/-- Converts the unreported messages to an array, oldest message first. -/
def toArray (log : MessageLog) : Array Message :=
log.msgs.toArray
log.unreported.toArray
end MessageLog

View File

@@ -304,7 +304,7 @@ structure State where
Backtrackable state for the `MetaM` monad.
-/
structure SavedState where
core : Core.State
core : Core.SavedState
meta : State
deriving Nonempty
@@ -410,20 +410,22 @@ instance : AddMessageContext MetaM where
addMessageContext := addMessageContextFull
protected def saveState : MetaM SavedState :=
return { core := ( getThe Core.State), meta := ( get) }
return { core := ( Core.saveState), meta := ( get) }
/-- Restore backtrackable parts of the state. -/
def SavedState.restore (b : SavedState) : MetaM Unit := do
Core.restore b.core
b.core.restore
modify fun s => { s with mctx := b.meta.mctx, zetaDeltaFVarIds := b.meta.zetaDeltaFVarIds, postponed := b.meta.postponed }
/--
Restores full state including sources for unique identifiers. Only intended for incremental reuse
between elaboration runs, not for backtracking within a single run.
-/
def SavedState.restoreFull (b : SavedState) : MetaM Unit := do
Core.restoreFull b.core
set b.meta
@[specialize, inherit_doc Core.withRestoreOrSaveFull]
def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState))
(cont : MetaM SavedState MetaM α) : MetaM α := do
if let some (_, state) := reusableResult? then
set state.meta
let reusableResult? := reusableResult?.map (fun (val, state) => (val, state.core))
controlAt CoreM fun runInCoreM =>
Core.withRestoreOrSaveFull reusableResult? fun restore =>
runInCoreM <| cont (return { core := ( restore), meta := ( get) })
instance : MonadBacktrack SavedState MetaM where
saveState := Meta.saveState

View File

@@ -14,13 +14,11 @@ private def getConstructorVal? (env : Environment) (ctorName : Name) : Option Co
| some (.ctorInfo v) => v
| _ => none
/--
If `e` is a constructor application or a builtin literal defeq to a constructor application,
If `e` is a constructor application,
then return the corresponding `ConstructorVal`.
-/
def isConstructorApp? (e : Expr) : MetaM (Option ConstructorVal) := do
let e litToCtor e
def isConstructorAppCore? (e : Expr) : MetaM (Option ConstructorVal) := do
let .const n _ := e.getAppFn | return none
let some v := getConstructorVal? ( getEnv) n | return none
if v.numParams + v.numFields == e.getAppNumArgs then
@@ -28,6 +26,13 @@ def isConstructorApp? (e : Expr) : MetaM (Option ConstructorVal) := do
else
return none
/--
If `e` is a constructor application or a builtin literal defeq to a constructor application,
then return the corresponding `ConstructorVal`.
-/
def isConstructorApp? (e : Expr) : MetaM (Option ConstructorVal) := do
isConstructorAppCore? ( litToCtor e)
/--
Similar to `isConstructorApp?`, but uses `whnf`.
-/

View File

@@ -99,6 +99,8 @@ def getUInt64Value? (e : Expr) : MetaM (Option UInt64) := OptionT.run do
let (n, _) getOfNatValue? e ``UInt64
return UInt64.ofNat n
-- TODO: extensibility
/--
If `e` is a literal value, ensure it is encoded using the standard representation.
Otherwise, just return `e`.
@@ -117,6 +119,23 @@ def normLitValue (e : Expr) : MetaM Expr := do
if let some n getUInt64Value? e then return toExpr n
return e
/--
Returns `true` if `e` is a literal value.
-/
def isLitValue (e : Expr) : MetaM Bool := do
let e instantiateMVars e
if ( getNatValue? e).isSome then return true
if ( getIntValue? e).isSome then return true
if ( getFinValue? e).isSome then return true
if ( getBitVecValue? e).isSome then return true
if (getStringValue? e).isSome then return true
if ( getCharValue? e).isSome then return true
if ( getUInt8Value? e).isSome then return true
if ( getUInt16Value? e).isSome then return true
if ( getUInt32Value? e).isSome then return true
if ( getUInt64Value? e).isSome then return true
return false
/--
If `e` is a `Nat`, `Int`, or `Fin` literal value, converts it into a constructor application.
Otherwise, just return `e`.

View File

@@ -19,14 +19,18 @@ def MatchEqns.size (e : MatchEqns) : Nat :=
structure MatchEqnsExtState where
map : PHashMap Name MatchEqns := {}
eqns : PHashSet Name := {}
deriving Inhabited
/- We generate the equations and splitter on demand, and do not save them on .olean files. -/
builtin_initialize matchEqnsExt : EnvExtension MatchEqnsExtState
registerEnvExtension (pure {})
def registerMatchEqns (matchDeclName : Name) (matchEqns : MatchEqns) : CoreM Unit :=
modifyEnv fun env => matchEqnsExt.modifyState env fun s => { s with map := s.map.insert matchDeclName matchEqns }
def registerMatchEqns (matchDeclName : Name) (matchEqns : MatchEqns) : CoreM Unit := do
modifyEnv fun env => matchEqnsExt.modifyState env fun { map, eqns } => {
eqns := matchEqns.eqnNames.foldl (init := eqns) fun eqns eqn => eqns.insert eqn
map := map.insert matchDeclName matchEqns
}
/-
Forward definition. We want to use `getEquationsFor` in the simplifier,
@@ -34,4 +38,10 @@ def registerMatchEqns (matchDeclName : Name) (matchEqns : MatchEqns) : CoreM Uni
@[extern "lean_get_match_equations_for"]
opaque getEquationsFor (matchDeclName : Name) : MetaM MatchEqns
/--
Returns `true` if `declName` is the name of a `match` equational theorem.
-/
def isMatchEqnTheorem (env : Environment) (declName : Name) : Bool :=
matchEqnsExt.getState env |>.eqns.contains declName
end Lean.Meta.Match

View File

@@ -200,7 +200,7 @@ private def toCasesSubgoals (s : Array InductionSubgoal) (ctorNames : Array Name
{ ctorName := ctorName,
toInductionSubgoal := s }
partial def unifyEqs? (numEqs : Nat) (mvarId : MVarId) (subst : FVarSubst) (caseName? : Option Name := none): MetaM (Option (MVarId × FVarSubst)) := do
partial def unifyEqs? (numEqs : Nat) (mvarId : MVarId) (subst : FVarSubst) (caseName? : Option Name := none): MetaM (Option (MVarId × FVarSubst)) := withIncRecDepth do
if numEqs == 0 then
return some (mvarId, subst)
else

View File

@@ -9,3 +9,6 @@ import Lean.Meta.Tactic.Grind.RevertAll
import Lean.Meta.Tactic.Grind.Types
import Lean.Meta.Tactic.Grind.Preprocessor
import Lean.Meta.Tactic.Grind.Util
import Lean.Meta.Tactic.Grind.Cases
import Lean.Meta.Tactic.Grind.Injection
import Lean.Meta.Tactic.Grind.Core

View File

@@ -16,4 +16,54 @@ builtin_initialize grindNormExt : SimpExtension ←
builtin_initialize grindNormSimprocExt : SimprocExtension
registerSimprocAttr `grind_norm_proc "simplification/normalization procedured for `grind`" none
builtin_initialize grindCasesExt : SimpleScopedEnvExtension Name NameSet
registerSimpleScopedEnvExtension {
initial := {}
addEntry := fun s declName => s.insert declName
}
/--
Returns `true` if `declName` has been tagged with attribute `[grind_cases]`.
-/
def isGrindCasesTarget (declName : Name) : CoreM Bool :=
return grindCasesExt.getState ( getEnv) |>.contains declName
private def getAlias? (value : Expr) : MetaM (Option Name) :=
lambdaTelescope value fun _ body => do
if let .const declName _ := body.getAppFn' then
return some declName
else
return none
/--
Throws an error if `declName` cannot be annotated with attribute `[grind_cases]`.
We support the following cases:
- `declName` is a non-recursive datatype.
- `declName` is an abbreviation for a non-recursive datatype.
-/
private partial def validateGrindCasesAttr (declName : Name) : CoreM Unit := do
match ( getConstInfo declName) with
| .inductInfo info =>
if info.isRec then
throwError "`{declName}` is a recursive datatype"
| .defnInfo info =>
let failed := throwError "`{declName}` is a definition, but it is not an alias/abbreviation for an inductive datatype"
let some declName getAlias? info.value |>.run' {} {}
| failed
try
validateGrindCasesAttr declName
catch _ =>
failed
| _ =>
throwError "`{declName}` is not an inductive datatype or an alias for one"
builtin_initialize
registerBuiltinAttribute {
name := `grind_cases
descr := "`grind` tactic applies `cases` to (non-recursive) inductives during pre-processing step"
add := fun declName _ attrKind => do
validateGrindCasesAttr declName
grindCasesExt.add declName attrKind
}
end Lean.Meta.Grind

View File

@@ -0,0 +1,62 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Lean.Meta.Tactic.Cases
namespace Lean.Meta.Grind
/--
The `grind` tactic includes an auxiliary `cases` tactic that is not intended for direct use by users.
This method implements it.
This tactic is automatically applied when introducing local declarations with a type tagged with `[grind_cases]`.
It differs from the user-facing Lean `cases` tactic in the following ways:
- It avoids unnecessary `revert` and `intro` operations.
- It does not introduce new local declarations for each minor premise. Instead, the `grind` tactic preprocessor is responsible for introducing them.
- It assumes that the major premise (i.e., the parameter `fvarId`) is the latest local declaration in the current goal.
- If the major premise type is an indexed family, auxiliary declarations and (heterogeneous) equalities are introduced.
However, these equalities are not resolved using `unifyEqs`. Instead, the `grind` tactic employs union-find and
congruence closure to process these auxiliary equalities. This approach avoids applying substitution to propositions
that have already been internalized by `grind`.
-/
def cases (mvarId : MVarId) (fvarId : FVarId) : MetaM (List MVarId) := mvarId.withContext do
let tag mvarId.getTag
let type whnf ( fvarId.getType)
let .const declName _ := type.getAppFn | throwInductiveExpected type
let .inductInfo _ getConstInfo declName | throwInductiveExpected type
let recursorInfo mkRecursorInfo (mkCasesOnName declName)
let k (mvarId : MVarId) (fvarId : FVarId) (indices : Array Expr) (clearMajor : Bool) : MetaM (List MVarId) := do
let recursor mkRecursorAppPrefix mvarId `grind.cases fvarId recursorInfo indices
let mut recursor := mkApp (mkAppN recursor indices) (mkFVar fvarId)
let mut recursorType inferType recursor
let mut mvarIdsNew := #[]
for _ in [:recursorInfo.numMinors] do
let .forallE _ targetNew recursorTypeNew _ whnf recursorType
| throwTacticEx `grind.cases mvarId "unexpected recursor type"
recursorType := recursorTypeNew
let mvar mkFreshExprSyntheticOpaqueMVar targetNew tag
recursor := mkApp recursor mvar
let mvarIdNew if clearMajor then
mvar.mvarId!.clear fvarId
else
pure mvar.mvarId!
mvarIdsNew := mvarIdsNew.push mvarIdNew
mvarId.assign recursor
return mvarIdsNew.toList
if recursorInfo.numIndices > 0 then
let s generalizeIndices mvarId fvarId
s.mvarId.withContext do
k s.mvarId s.fvarId (s.indicesFVarIds.map mkFVar) (clearMajor := false)
else
let indices getMajorTypeIndices mvarId `grind.cases recursorInfo type
k mvarId fvarId indices (clearMajor := true)
where
throwInductiveExpected {α} (type : Expr) : MetaM α := do
throwTacticEx `grind.cases mvarId m!"(non-recursive) inductive type expected at {mkFVar fvarId}{indentExpr type}"
end Lean.Meta.Grind

View File

@@ -0,0 +1,157 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Lean.Meta.Tactic.Grind.Types
import Lean.Meta.LitValues
namespace Lean.Meta.Grind
/--
Returns `true` if `e` is `True`, `False`, or a literal value.
See `LitValues` for supported literals.
-/
def isInterpreted (e : Expr) : MetaM Bool := do
if e.isTrue || e.isFalse then return true
isLitValue e
/--
Creates an `ENode` for `e` if one does not already exist.
This method assumes `e` has been hashconsed.
-/
def mkENode (e : Expr) (generation : Nat := 0) : GoalM Unit := do
if ( getENode? e).isSome then return ()
let ctor := ( isConstructorAppCore? e).isSome
let interpreted isInterpreted e
mkENodeCore e interpreted ctor generation
/--
Returns the root element in the equivalence class of `e`.
-/
def getRoot (e : Expr) : GoalM Expr := do
let some n getENode? e | return e
return n.root
/--
Returns the next element in the equivalence class of `e`.
-/
def getNext (e : Expr) : GoalM Expr := do
let some n getENode? e | return e
return n.next
@[inline] def isSameExpr (a b : Expr) : Bool :=
-- It is safe to use pointer equality because we hashcons all expressions
-- inserted into the E-graph
unsafe ptrEq a b
private def pushNewEqCore (lhs rhs proof : Expr) (isHEq : Bool) : GoalM Unit :=
modify fun s => { s with newEqs := s.newEqs.push { lhs, rhs, proof, isHEq } }
@[inline] private def pushNewEq (lhs rhs proof : Expr) : GoalM Unit :=
pushNewEqCore lhs rhs proof (isHEq := false)
@[inline] private def pushNewHEq (lhs rhs proof : Expr) : GoalM Unit :=
pushNewEqCore lhs rhs proof (isHEq := true)
/--
The fields `target?` and `proof?` in `e`'s `ENode` are encoding a transitivity proof
from `e` to the root of the equivalence class
This method "inverts" the proof, and makes it to go from the root of the equivalence class to `e`.
We use this method when merging two equivalence classes.
-/
private partial def invertTrans (e : Expr) : GoalM Unit := do
go e false none none
where
go (e : Expr) (flippedNew : Bool) (targetNew? : Option Expr) (proofNew? : Option Expr) : GoalM Unit := do
let some node getENode? e | unreachable!
if let some target := node.target? then
go target (!node.flipped) (some e) node.proof?
setENode e { node with
target? := targetNew?
flipped := flippedNew
proof? := proofNew?
}
private def markAsInconsistent : GoalM Unit :=
modify fun s => { s with inconsistent := true }
private partial def addEqStep (lhs rhs proof : Expr) (isHEq : Bool) : GoalM Unit := do
let some lhsNode getENode? lhs | return () -- `lhs` has not been internalized yet
let some rhsNode getENode? rhs | return () -- `rhs` has not been internalized yet
if isSameExpr lhsNode.root rhsNode.root then return () -- `lhs` and `rhs` are already in the same equivalence class.
let some lhsRoot getENode? lhsNode.root | unreachable!
let some rhsRoot getENode? rhsNode.root | unreachable!
if (lhsRoot.interpreted && !rhsRoot.interpreted)
|| (lhsRoot.ctor && !rhsRoot.ctor)
|| (lhsRoot.size > rhsRoot.size && !rhsRoot.interpreted && !rhsRoot.ctor) then
go rhs lhs rhsNode lhsNode rhsRoot lhsRoot true
else
go lhs rhs lhsNode rhsNode lhsRoot rhsRoot false
where
go (lhs rhs : Expr) (lhsNode rhsNode lhsRoot rhsRoot : ENode) (flipped : Bool) : GoalM Unit := do
let mut valueInconsistency := false
if lhsRoot.interpreted && rhsRoot.interpreted then
if lhsNode.root.isTrue || rhsNode.root.isTrue then
markAsInconsistent
else
valueInconsistency := true
-- TODO: process valueInconsistency := true
/-
We have the following `target?/proof?`
`lhs -> ... -> lhsNode.root`
`rhs -> ... -> rhsNode.root`
We want to convert it to
`lhsNode.root -> ... -> lhs -*-> rhs -> ... -> rhsNode.root`
where step `-*->` is justified by `proof` (or `proof.symm` if `flipped := true`)
-/
invertTrans lhs
setENode lhs { lhsNode with
target? := rhs
proof? := proof
flipped
}
-- TODO: Remove parents from congruence table
-- TODO: set propagateBool
updateRoots lhs rhsNode.root true -- TODO
-- TODO: Reinsert parents into congruence table
setENode lhsNode.root { lhsRoot with
next := rhsRoot.next
}
setENode rhsNode.root { rhsRoot with
next := lhsRoot.next
size := rhsRoot.size + lhsRoot.size
hasLambdas := rhsRoot.hasLambdas || lhsRoot.hasLambdas
heqProofs := isHEq || rhsRoot.heqProofs || lhsRoot.heqProofs
}
-- TODO: copy parentst from lhsRoot parents to rhsRoot parents
updateRoots (lhs : Expr) (rootNew : Expr) (_propagateBool : Bool) : GoalM Unit := do
let rec loop (e : Expr) : GoalM Unit := do
-- TODO: propagateBool
let some n getENode? e | unreachable!
setENode e { n with root := rootNew }
if isSameExpr lhs n.next then return ()
loop n.next
loop lhs
partial def addEqCore (lhs rhs proof : Expr) (isHEq : Bool) : GoalM Unit := do
addEqStep lhs rhs proof isHEq
processTodo
where
processTodo : GoalM Unit := do
if ( get).inconsistent then
modify fun s => { s with newEqs := #[] }
return ()
let some { lhs, rhs, proof, isHEq } := ( get).newEqs.back? | return ()
addEqStep lhs rhs proof isHEq
processTodo
def addEq (lhs rhs proof : Expr) : GoalM Unit := do
addEqCore lhs rhs proof false
def addHEq (lhs rhs proof : Expr) : GoalM Unit := do
addEqCore lhs rhs proof true
end Lean.Meta.Grind

View File

@@ -0,0 +1,39 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Lean.Meta.CtorRecognizer
import Lean.Meta.Tactic.Util
import Lean.Meta.Tactic.Clear
namespace Lean.Meta.Grind
/--
The `grind` tactic includes an auxiliary `injection?` tactic that is not intended for direct use by users.
This tactic is automatically applied when introducing non-dependent propositions.
It differs from the user-facing Lean `injection` tactic in the following ways:
- It does not introduce new propositions. Instead, the `grind` tactic preprocessor is responsible for introducing them.
- It assumes that `fvarId` is the latest local declaration in the current goal.
- It does not handle cases where the constructors are different because the simplifier already reduces such equalities to `False`.
- It does not have support for heterogeneous equality. Recall that the simplifier already reduces them to `Eq` if
the types are definitionally equal.
-/
def injection? (mvarId : MVarId) (fvarId : FVarId) : MetaM (Option MVarId) := mvarId.withContext do
let decl fvarId.getDecl
let_expr Eq _ a b := decl.type | return none
match ( isConstructorAppCore? a), ( isConstructorAppCore? b) with
| some aCtor, some bCtor =>
if aCtor.name != bCtor.name then return none -- Simplifier handles this case
let target mvarId.getType
let val mkNoConfusion target (mkFVar fvarId)
let .forallE _ targetNew _ _ whnfD ( inferType val) | return none
let targetNew := targetNew.headBeta
let tag mvarId.getTag
let mvarNew mkFreshExprSyntheticOpaqueMVar targetNew tag
mvarId.assign (mkApp val mvarNew)
return some ( mvarNew.mvarId!.clear fvarId)
| _, _ => return none
end Lean.Meta.Grind

View File

@@ -13,6 +13,9 @@ import Lean.Meta.Tactic.Grind.Attr
import Lean.Meta.Tactic.Grind.RevertAll
import Lean.Meta.Tactic.Grind.Types
import Lean.Meta.Tactic.Grind.Util
import Lean.Meta.Tactic.Grind.Cases
import Lean.Meta.Tactic.Grind.Injection
import Lean.Meta.Tactic.Grind.Core
namespace Lean.Meta.Grind
namespace Preprocessor
@@ -27,6 +30,7 @@ structure Context where
structure State where
simpStats : Simp.Stats := {}
goals : PArray Goal := {}
deriving Inhabited
abbrev PreM := ReaderT Context $ StateRefT State GrindM
@@ -41,21 +45,17 @@ def PreM.run (x : PreM α) : GrindM α := do
}
x { simp, simprocs } |>.run' {}
def simp (e : Expr) : PreM Simp.Result := do
def simp (_goal : Goal) (e : Expr) : PreM Simp.Result := do
-- TODO: use `goal` state in the simplifier
let simpStats := ( get).simpStats
let (r, simpStats) Meta.simp e ( read).simp ( read).simprocs (stats := simpStats)
modify fun s => { s with simpStats }
return r
def simpHyp? (mvarId : MVarId) (fvarId : FVarId) : PreM (Option (FVarId × MVarId)) := do
let simpStats := ( get).simpStats
let (result, simpStats) simpLocalDecl mvarId fvarId ( read).simp ( read).simprocs (stats := simpStats)
modify fun s => { s with simpStats }
return result
inductive IntroResult where
| done
| newHyp (fvarId : FVarId) (goal : Goal)
| newDepHyp (goal : Goal)
| newLocal (fvarId : FVarId) (goal : Goal)
def introNext (goal : Goal) : PreM IntroResult := do
@@ -69,7 +69,7 @@ def introNext (goal : Goal) : PreM IntroResult := do
else
let tag goal.mvarId.getTag
let q := target.bindingBody!
let r simp p
let r simp goal p
let p' := r.expr
let p' canon p'
let p' shareCommon p'
@@ -95,44 +95,73 @@ def introNext (goal : Goal) : PreM IntroResult := do
if ( isProp localDecl.type) then
-- Add a non-dependent copy
let mvarId mvarId.assert localDecl.userName localDecl.type (mkFVar fvarId)
return .newLocal fvarId { goal with mvarId }
return .newDepHyp { goal with mvarId }
else
return .newLocal fvarId { goal with mvarId }
else
return .done
def pushResult (goal : Goal) : PreM Unit :=
modifyThe Grind.State fun s => { s with goals := s.goals.push goal }
modify fun s => { s with goals := s.goals.push goal }
partial def preprocess (goal : Goal) : PreM Unit := do
trace[Meta.debug] "{goal.mvarId}"
def isCasesCandidate (fvarId : FVarId) : MetaM Bool := do
let .const declName _ := ( fvarId.getType).getAppFn | return false
isGrindCasesTarget declName
def applyCases? (goal : Goal) (fvarId : FVarId) : MetaM (Option (List Goal)) := goal.mvarId.withContext do
if ( isCasesCandidate fvarId) then
let mvarIds cases goal.mvarId fvarId
return mvarIds.map fun mvarId => { goal with mvarId }
else
return none
def applyInjection? (goal : Goal) (fvarId : FVarId) : MetaM (Option Goal) := do
if let some mvarId injection? goal.mvarId fvarId then
return some { goal with mvarId }
else
return none
partial def loop (goal : Goal) : PreM Unit := do
match ( introNext goal) with
| .done =>
if let some mvarId goal.mvarId.byContra? then
preprocess { goal with mvarId }
loop { goal with mvarId }
else
pushResult goal
| .newHyp fvarId goal =>
-- TODO: apply eliminators
let clause goal.mvarId.withContext do mkInputClause fvarId
preprocess { goal with clauses := goal.clauses.push clause }
| .newLocal _ goal =>
-- TODO: apply eliminators
preprocess goal
if let some goals applyCases? goal fvarId then
goals.forM loop
else if let some goal applyInjection? goal fvarId then
loop goal
else
let clause goal.mvarId.withContext do mkInputClause fvarId
loop { goal with clauses := goal.clauses.push clause }
| .newDepHyp goal =>
loop goal
| .newLocal fvarId goal =>
if let some goals applyCases? goal fvarId then
goals.forM loop
else
loop goal
def preprocess (mvarId : MVarId) : PreM State := do
loop ( mkGoal mvarId)
get
end Preprocessor
open Preprocessor
partial def main (mvarId : MVarId) (mainDeclName : Name) : MetaM Grind.State := do
partial def main (mvarId : MVarId) (mainDeclName : Name) : MetaM (List MVarId) := do
mvarId.ensureProp
mvarId.ensureNoMVar
let mvarId mvarId.clearAuxDecls
let mvarId mvarId.revertAll
mvarId.ensureNoMVar
let mvarId mvarId.abstractNestedProofs mainDeclName
let mvarId mvarId.unfoldReducible
let mvarId mvarId.betaReduce
let s (preprocess { mvarId } *> getThe Grind.State) |>.run |>.run mainDeclName
return s
let s preprocess mvarId |>.run |>.run mainDeclName
return s.goals.toList.map (·.mvarId)
end Lean.Meta.Grind

View File

@@ -11,6 +11,47 @@ import Lean.Meta.Canonicalizer
import Lean.Meta.Tactic.Util
namespace Lean.Meta.Grind
structure Context where
mainDeclName : Name
structure State where
canon : Canonicalizer.State := {}
/-- `ShareCommon` (aka `Hashconsing`) state. -/
scState : ShareCommon.State.{0} ShareCommon.objectFactory := ShareCommon.State.mk _
/-- Next index for creating auxiliary theorems. -/
nextThmIdx : Nat := 1
abbrev GrindM := ReaderT Context $ StateRefT State MetaM
@[inline] def GrindM.run (x : GrindM α) (mainDeclName : Name) : MetaM α :=
x { mainDeclName } |>.run' {}
def abstractNestedProofs (e : Expr) : GrindM Expr := do
let nextIdx := ( get).nextThmIdx
let (e, s') AbstractNestedProofs.visit e |>.run { baseName := ( read).mainDeclName } |>.run |>.run { nextIdx }
modify fun s => { s with nextThmIdx := s'.nextIdx }
return e
def shareCommon (e : Expr) : GrindM Expr := do
modifyGet fun { canon, scState, nextThmIdx } =>
let (e, scState) := ShareCommon.State.shareCommon scState e
(e, { canon, scState, nextThmIdx })
def canon (e : Expr) : GrindM Expr := do
let canonS modifyGet fun s => (s.canon, { s with canon := {} })
let (e, canonS) Canonicalizer.CanonM.run (canonRec e) (s := canonS)
modify fun s => { s with canon := canonS }
return e
where
canonRec (e : Expr) : CanonM Expr := do
let post (e : Expr) : CanonM TransformStep := do
if e.isApp then
return .done ( Meta.canon e)
else
return .done e
transform e post
/--
Stores information for a node in the egraph.
Each internalized expression `e` has an `ENode` associated with it.
@@ -43,6 +84,9 @@ structure ENode where
on heterogeneous equality.
-/
heqProofs : Bool := false
generation : Nat := 0
/-- Modification time -/
mt : Nat := 0
-- TODO: see Lean 3 implementation
structure Clause where
@@ -53,58 +97,57 @@ structure Clause where
def mkInputClause (fvarId : FVarId) : MetaM Clause :=
return { expr := ( fvarId.getType), proof := mkFVar fvarId }
structure Goal where
mvarId : MVarId
clauses : PArray Clause := {}
enodes : PHashMap UInt64 ENode := {}
-- TODO: occurrences for propagation, etc
deriving Inhabited
structure NewEq where
lhs : Expr
rhs : Expr
proof : Expr
isHEq : Bool
def mkGoal (mvarId : MVarId) : Goal :=
{ mvarId }
structure Goal where
mvarId : MVarId
clauses : PArray Clause := {}
enodes : PHashMap USize ENode := {}
newEqs : Array NewEq := #[]
/-- `inconsistent := true` if `ENode`s for `True` and `False` are in the same equivalence class. -/
inconsistent : Bool := false
/-- Goal modification time. -/
gmt : Nat := 0
deriving Inhabited
def Goal.admit (goal : Goal) : MetaM Unit :=
goal.mvarId.admit
structure Context where
mainDeclName : Name
abbrev GoalM := StateRefT Goal GrindM
structure State where
canon : Canonicalizer.State := {}
/-- `ShareCommon` (aka `Hashconsing`) state. -/
scState : ShareCommon.State.{0} ShareCommon.objectFactory := ShareCommon.State.mk _
/-- Next index for creating auxiliary theorems. -/
nextThmIdx : Nat := 1
goals : PArray Goal := {}
@[inline] def GoalM.run (goal : Goal) (x : GoalM α) : GrindM (α × Goal) :=
StateRefT'.run x goal
abbrev GrindM := ReaderT Context $ StateRefT State MetaM
@[inline] def GoalM.run' (goal : Goal) (x : GoalM Unit) : GrindM Goal :=
StateRefT'.run' (x *> get) goal
def GrindM.run (x : GrindM α) (mainDeclName : Name) : MetaM α :=
x { mainDeclName } |>.run' {}
/--
Returns `some n` if `e` has already been "internalized" into the
Otherwise, returns `none`s.
-/
def getENode? (e : Expr) : GoalM (Option ENode) :=
return ( get).enodes.find? (unsafe ptrAddrUnsafe e)
def abstractNestedProofs (e : Expr) : GrindM Expr := do
let nextIdx := ( get).nextThmIdx
let (e, s') AbstractNestedProofs.visit e |>.run { baseName := ( read).mainDeclName } |>.run |>.run { nextIdx }
modify fun s => { s with nextThmIdx := s'.nextIdx }
return e
def setENode (e : Expr) (n : ENode) : GoalM Unit :=
modify fun s => { s with enodes := s.enodes.insert (unsafe ptrAddrUnsafe e) n }
def shareCommon (e : Expr) : GrindM Expr := do
modifyGet fun { canon, scState, nextThmIdx, goals } =>
let (e, scState) := ShareCommon.State.shareCommon scState e
(e, { canon, scState, nextThmIdx, goals })
def mkENodeCore (e : Expr) (interpreted ctor : Bool) (generation : Nat) : GoalM Unit := do
setENode e {
next := e, root := e, cgRoot := e, size := 1
flipped := false
heqProofs := false
hasLambdas := e.isLambda
mt := ( get).gmt
interpreted, ctor, generation
}
def canon (e : Expr) : GrindM Expr := do
let canonS modifyGet fun s => (s.canon, { s with canon := {} })
let (e, canonS) Canonicalizer.CanonM.run (canonRec e) (s := canonS)
modify fun s => { s with canon := canonS }
return e
where
canonRec (e : Expr) : CanonM Expr := do
let post (e : Expr) : CanonM TransformStep := do
if e.isApp then
return .done ( Meta.canon e)
else
return .done e
transform e post
def mkGoal (mvarId : MVarId) : GrindM Goal := do
GoalM.run' { mvarId } do
mkENodeCore ( shareCommon (mkConst ``True)) (interpreted := true) (ctor := false) (generation := 0)
mkENodeCore ( shareCommon (mkConst ``False)) (interpreted := true) (ctor := false) (generation := 0)
end Lean.Meta.Grind

View File

@@ -6,6 +6,7 @@ Authors: Leonardo de Moura
prelude
import Lean.Meta.AbstractNestedProofs
import Lean.Meta.Tactic.Util
import Lean.Meta.Tactic.Clear
namespace Lean.Meta.Grind
/--
@@ -66,7 +67,7 @@ def _root_.Lean.MVarId.betaReduce (mvarId : MVarId) : MetaM MVarId :=
If the target is not `False`, apply `byContradiction`.
-/
def _root_.Lean.MVarId.byContra? (mvarId : MVarId) : MetaM (Option MVarId) := mvarId.withContext do
mvarId.checkNotAssigned `grind
mvarId.checkNotAssigned `grind.by_contra
let target mvarId.getType
if target.isFalse then return none
let targetNew mkArrow (mkNot target) (mkConst ``False)
@@ -75,4 +76,24 @@ def _root_.Lean.MVarId.byContra? (mvarId : MVarId) : MetaM (Option MVarId) := mv
mvarId.assign <| mkApp2 (mkConst ``Classical.byContradiction) target mvarNew
return mvarNew.mvarId!
/--
Clear auxiliary decls used to encode recursive declarations.
`grind` eliminates them to ensure they are not accidentaly used by its proof automation.
-/
def _root_.Lean.MVarId.clearAuxDecls (mvarId : MVarId) : MetaM MVarId := mvarId.withContext do
mvarId.checkNotAssigned `grind.clear_aux_decls
let mut toClear := []
for localDecl in ( getLCtx) do
if localDecl.isAuxDecl then
toClear := localDecl.fvarId :: toClear
if toClear.isEmpty then
return mvarId
let mut mvarId := mvarId
for fvarId in toClear do
try
mvarId mvarId.clear fvarId
catch _ =>
throwTacticEx `grind.clear_aux_decls mvarId "failed to clear local auxiliary declaration"
return mvarId
end Lean.Meta.Grind

View File

@@ -120,8 +120,75 @@ private partial def finalize
pure subgoals
loop (recursorInfo.paramsPos.length + 1) 0 recursor recursorType false #[]
private def throwUnexpectedMajorType {α} (mvarId : MVarId) (majorType : Expr) : MetaM α :=
throwTacticEx `induction mvarId m!"unexpected major premise type{indentExpr majorType}"
private def throwUnexpectedMajorType (tacticName : Name) (mvarId : MVarId) (majorType : Expr) : MetaM α :=
throwTacticEx tacticName mvarId m!"unexpected major premise type{indentExpr majorType}"
/--
Auxiliary method for implementing `induction`-like tactics.
It retrieves indices from `majorType` using `recursorInfo`.
Remark: `mvarId` and `tacticName` are used to generate error messages.
-/
def getMajorTypeIndices (mvarId : MVarId) (tacticName : Name) (recursorInfo : RecursorInfo) (majorType : Expr) : MetaM (Array Expr) := do
let majorTypeArgs := majorType.getAppArgs
recursorInfo.indicesPos.toArray.mapM fun idxPos => do
if idxPos majorTypeArgs.size then throwTacticEx tacticName mvarId m!"major premise type is ill-formed{indentExpr majorType}"
let idx := majorTypeArgs.get! idxPos
unless idx.isFVar do throwTacticEx tacticName mvarId m!"major premise type index {idx} is not a variable{indentExpr majorType}"
majorTypeArgs.size.forM fun i => do
let arg := majorTypeArgs[i]!
if i != idxPos && arg == idx then
throwTacticEx tacticName mvarId m!"'{idx}' is an index in major premise, but it occurs more than once{indentExpr majorType}"
if i < idxPos then
if ( exprDependsOn arg idx.fvarId!) then
throwTacticEx tacticName mvarId m!"'{idx}' is an index in major premise, but it occurs in previous arguments{indentExpr majorType}"
-- If arg is also and index and a variable occurring after `idx`, we need to make sure it doesn't depend on `idx`.
-- Note that if `arg` is not a variable, we will fail anyway when we visit it.
if i > idxPos && recursorInfo.indicesPos.contains i && arg.isFVar then
let idxDecl idx.fvarId!.getDecl
if ( localDeclDependsOn idxDecl arg.fvarId!) then
throwTacticEx tacticName mvarId m!"'{idx}' is an index in major premise, but it depends on index occurring at position #{i+1}"
return idx
/--
Auxiliary method for implementing `induction`-like tactics.
It creates the prefix of a recursor application up-to `motive`.
The motive is computed by abstracting `major` and `indices` at `mvarId.getType`.
It retrieves indices from `majorType` using `recursorInfo`.
Remark: `mvarId` and `tacticName` are used to generate error messages.
-/
def mkRecursorAppPrefix (mvarId : MVarId) (tacticName : Name) (majorFVarId : FVarId) (recursorInfo : RecursorInfo) (indices : Array Expr) : MetaM Expr := do
let major := mkFVar majorFVarId
let target mvarId.getType
let targetLevel getLevel target
let targetLevel normalizeLevel targetLevel
let majorLocalDecl majorFVarId.getDecl
let some majorType whnfUntil majorLocalDecl.type recursorInfo.typeName
| throwUnexpectedMajorType tacticName mvarId majorLocalDecl.type
majorType.withApp fun majorTypeFn majorTypeArgs => do
match majorTypeFn with
| .const _ majorTypeFnLevels => do
let majorTypeFnLevels := majorTypeFnLevels.toArray
let (recursorLevels, foundTargetLevel) recursorInfo.univLevelPos.foldlM (init := (#[], false))
fun (recursorLevels, foundTargetLevel) (univPos : RecursorUnivLevelPos) => do
match univPos with
| RecursorUnivLevelPos.motive => pure (recursorLevels.push targetLevel, true)
| RecursorUnivLevelPos.majorType idx =>
if idx majorTypeFnLevels.size then throwTacticEx tacticName mvarId "ill-formed recursor"
pure (recursorLevels.push (majorTypeFnLevels.get! idx), foundTargetLevel)
if !foundTargetLevel && !targetLevel.isZero then
throwTacticEx tacticName mvarId m!"recursor '{recursorInfo.recursorName}' can only eliminate into Prop"
let recursor := mkConst recursorInfo.recursorName recursorLevels.toList
let recursor addRecParams mvarId majorTypeArgs recursorInfo.paramsPos recursor
-- Compute motive
let motive := target
let motive if recursorInfo.depElim then
pure <| mkLambda `x BinderInfo.default ( inferType major) ( motive.abstractM #[major])
else
pure motive
let motive mkLambdaFVars indices motive
return mkApp recursor motive
| _ =>
throwTacticEx tacticName mvarId "major premise is not of the form (C ...)"
def _root_.Lean.MVarId.induction (mvarId : MVarId) (majorFVarId : FVarId) (recursorName : Name) (givenNames : Array AltVarNames := #[]) : MetaM (Array InductionSubgoal) :=
mvarId.withContext do
@@ -129,30 +196,14 @@ def _root_.Lean.MVarId.induction (mvarId : MVarId) (majorFVarId : FVarId) (recur
mvarId.checkNotAssigned `induction
let majorLocalDecl majorFVarId.getDecl
let recursorInfo mkRecursorInfo recursorName
let some majorType whnfUntil majorLocalDecl.type recursorInfo.typeName | throwUnexpectedMajorType mvarId majorLocalDecl.type
let some majorType whnfUntil majorLocalDecl.type recursorInfo.typeName
| throwUnexpectedMajorType `induction mvarId majorLocalDecl.type
majorType.withApp fun _ majorTypeArgs => do
recursorInfo.paramsPos.forM fun paramPos? => do
match paramPos? with
| none => pure ()
| some paramPos => if paramPos majorTypeArgs.size then throwTacticEx `induction mvarId m!"major premise type is ill-formed{indentExpr majorType}"
let indices recursorInfo.indicesPos.toArray.mapM fun idxPos => do
if idxPos majorTypeArgs.size then throwTacticEx `induction mvarId m!"major premise type is ill-formed{indentExpr majorType}"
let idx := majorTypeArgs.get! idxPos
unless idx.isFVar do throwTacticEx `induction mvarId m!"major premise type index {idx} is not a variable{indentExpr majorType}"
majorTypeArgs.size.forM fun i => do
let arg := majorTypeArgs[i]!
if i != idxPos && arg == idx then
throwTacticEx `induction mvarId m!"'{idx}' is an index in major premise, but it occurs more than once{indentExpr majorType}"
if i < idxPos then
if ( exprDependsOn arg idx.fvarId!) then
throwTacticEx `induction mvarId m!"'{idx}' is an index in major premise, but it occurs in previous arguments{indentExpr majorType}"
-- If arg is also and index and a variable occurring after `idx`, we need to make sure it doesn't depend on `idx`.
-- Note that if `arg` is not a variable, we will fail anyway when we visit it.
if i > idxPos && recursorInfo.indicesPos.contains i && arg.isFVar then
let idxDecl idx.fvarId!.getDecl
if ( localDeclDependsOn idxDecl arg.fvarId!) then
throwTacticEx `induction mvarId m!"'{idx}' is an index in major premise, but it depends on index occurring at position #{i+1}"
pure idx
let indices getMajorTypeIndices mvarId `induction recursorInfo majorType
let target mvarId.getType
if ( pure !recursorInfo.depElim <&&> exprDependsOn target majorFVarId) then
throwTacticEx `induction mvarId m!"recursor '{recursorName}' does not support dependent elimination, but conclusion depends on major premise"
@@ -175,37 +226,8 @@ def _root_.Lean.MVarId.induction (mvarId : MVarId) (majorFVarId : FVarId) (recur
let majorFVarId := majorFVarId'
let major := mkFVar majorFVarId
mvarId.withContext do
let target mvarId.getType
let targetLevel getLevel target
let targetLevel normalizeLevel targetLevel
let majorLocalDecl majorFVarId.getDecl
let some majorType whnfUntil majorLocalDecl.type recursorInfo.typeName | throwUnexpectedMajorType mvarId majorLocalDecl.type
majorType.withApp fun majorTypeFn majorTypeArgs => do
match majorTypeFn with
| Expr.const _ majorTypeFnLevels => do
let majorTypeFnLevels := majorTypeFnLevels.toArray
let (recursorLevels, foundTargetLevel) recursorInfo.univLevelPos.foldlM (init := (#[], false))
fun (recursorLevels, foundTargetLevel) (univPos : RecursorUnivLevelPos) => do
match univPos with
| RecursorUnivLevelPos.motive => pure (recursorLevels.push targetLevel, true)
| RecursorUnivLevelPos.majorType idx =>
if idx majorTypeFnLevels.size then throwTacticEx `induction mvarId "ill-formed recursor"
pure (recursorLevels.push (majorTypeFnLevels.get! idx), foundTargetLevel)
if !foundTargetLevel && !targetLevel.isZero then
throwTacticEx `induction mvarId m!"recursor '{recursorName}' can only eliminate into Prop"
let recursor := mkConst recursorName recursorLevels.toList
let recursor addRecParams mvarId majorTypeArgs recursorInfo.paramsPos recursor
-- Compute motive
let motive := target
let motive if recursorInfo.depElim then
pure <| mkLambda `x BinderInfo.default ( inferType major) ( motive.abstractM #[major])
else
pure motive
let motive mkLambdaFVars indices motive
let recursor := mkApp recursor motive
finalize mvarId givenNames recursorInfo reverted major indices baseSubst recursor
| _ =>
throwTacticEx `induction mvarId "major premise is not of the form (C ...)"
let recursor mkRecursorAppPrefix mvarId `induction majorFVarId recursorInfo indices
finalize mvarId givenNames recursorInfo reverted major indices baseSubst recursor
@[deprecated MVarId.induction (since := "2022-07-15")]
def induction (mvarId : MVarId) (majorFVarId : FVarId) (recursorName : Name) (givenNames : Array AltVarNames := #[]) : MetaM (Array InductionSubgoal) :=

View File

@@ -221,13 +221,14 @@ partial def SimpTheorems.eraseCore (d : SimpTheorems) (thmId : Origin) : SimpThe
else
d
def SimpTheorems.erase [Monad m] [MonadError m] (d : SimpTheorems) (thmId : Origin) : m SimpTheorems := do
def SimpTheorems.erase [Monad m] [MonadLog m] [AddMessageContext m] [MonadOptions m]
(d : SimpTheorems) (thmId : Origin) : m SimpTheorems := do
unless d.isLemma thmId ||
match thmId with
| .decl declName .. => d.isDeclToUnfold declName || d.toUnfoldThms.contains declName
| _ => false
do
throwError "'{thmId.key}' does not have [simp] attribute"
logWarning m!"'{thmId.key}' does not have [simp] attribute"
return d.eraseCore thmId
private partial def isPerm : Expr Expr MetaM Bool

View File

@@ -21,6 +21,11 @@ structure UnifyEqResult where
subst : FVarSubst
numNewEqs : Nat := 0
private def toOffset? (e : Expr) : MetaM (Option (Expr × Nat)) := do
match ( evalNat e) with
| some k => return some (mkNatLit 0, k)
| none => isOffset? e
/--
Helper method for methods such as `Cases.unifyEqs?`.
Given the given goal `mvarId` containing the local hypothesis `eqFVarId`, it performs the following operations:
@@ -69,7 +74,30 @@ def unifyEq? (mvarId : MVarId) (eqFVarId : FVarId) (subst : FVarSubst := {})
return none -- this alternative has been solved
else
throwError "dependent elimination failed, failed to solve equation{indentExpr eqDecl.type}"
/- Special support for offset equalities -/
let injectionOffset? (a b : Expr) := do
unless ( getEnv).contains ``Nat.elimOffset do return none
let some (xa, ka) toOffset? a | return none
let some (xb, kb) toOffset? b | return none
if ka == 0 || kb == 0 then return none -- use default noConfusion
let (x, y, k) if ka < kb then
pure (xa, ( mkAdd xb (mkNatLit (kb - ka))), ka)
else if ka = kb then
pure (xa, xb, ka)
else
pure (( mkAdd xa (mkNatLit (ka - kb))), xb, kb)
let target mvarId.getType
let u getLevel target
let newTarget mkArrow ( mkEq x y) target
let tag mvarId.getTag
let newMVar mkFreshExprSyntheticOpaqueMVar newTarget tag
let val := mkAppN (mkConst ``Nat.elimOffset [u]) #[target, x, y, mkNatLit k, eqDecl.toExpr, newMVar]
mvarId.assign val
let mvarId newMVar.mvarId!.tryClear eqDecl.fvarId
return some mvarId
let rec injection (a b : Expr) := do
if let some mvarId injectionOffset? a b then
return some { mvarId, numNewEqs := 1, subst }
if ( isConstructorApp a <&&> isConstructorApp b) then
/- ctor_i ... = ctor_j ... -/
match ( injectionCore mvarId eqFVarId) with

View File

@@ -558,6 +558,8 @@ partial def whitespace : ParserFn := fun c s =>
let curr := input.get' i h
if curr == '\t' then
s.mkUnexpectedError (pushMissing := false) "tabs are not allowed; please configure your editor to expand them"
else if curr == '\r' then
s.mkUnexpectedError (pushMissing := false) "isolated carriage returns are not allowed"
else if curr.isWhitespace then whitespace c (s.next' input i h)
else if curr == '-' then
let i := input.next' i h
@@ -621,9 +623,8 @@ def hexDigitFn : ParserFn := fun c s =>
/--
Parses the whitespace after the `\` when there is a string gap.
Raises an error if the whitespace does not contain exactly one newline character.
Processes `\r\n` as a newline.
-/
partial def stringGapFn (seenNewline afterCR : Bool) : ParserFn := fun c s =>
partial def stringGapFn (seenNewline : Bool) : ParserFn := fun c s =>
let i := s.pos
if h : c.input.atEnd i then s -- let strLitFnAux handle the EOI error if !seenNewline
else
@@ -633,13 +634,9 @@ partial def stringGapFn (seenNewline afterCR : Bool) : ParserFn := fun c s =>
-- Having more than one newline in a string gap is visually confusing
s.mkUnexpectedError "unexpected additional newline in string gap"
else
stringGapFn true false c (s.next' c.input i h)
else if curr == '\r' then
stringGapFn seenNewline true c (s.next' c.input i h)
else if afterCR then
s.mkUnexpectedError "expecting newline after carriage return"
stringGapFn true c (s.next' c.input i h)
else if curr.isWhitespace then
stringGapFn seenNewline false c (s.next' c.input i h)
stringGapFn seenNewline c (s.next' c.input i h)
else if seenNewline then
s
else
@@ -663,8 +660,8 @@ def quotedCharCoreFn (isQuotable : Char → Bool) (inString : Bool) : ParserFn :
andthenFn hexDigitFn hexDigitFn c (s.next' input i h)
else if curr == 'u' then
andthenFn hexDigitFn (andthenFn hexDigitFn (andthenFn hexDigitFn hexDigitFn)) c (s.next' input i h)
else if inString && (curr == '\n' || curr == '\r') then
stringGapFn false false c s
else if inString && curr == '\n' then
stringGapFn false c s
else
s.mkUnexpectedError "invalid escape sequence"

View File

@@ -94,8 +94,41 @@ def declSig := leading_parser
/-- `optDeclSig` matches the signature of a declaration with optional type: a list of binders and then possibly `: type` -/
def optDeclSig := leading_parser
many (ppSpace >> (Term.binderIdent <|> Term.bracketedBinder)) >> Term.optType
/-- Right-hand side of a `:=` in a declaration, a term. -/
def declBody : Parser :=
/-
We want to make sure that bodies starting with `by` in fact create a single `by` node instead of
accidentally parsing some remnants after it as well. This can especially happen when starting to
type comments inside tactic blocks where
```
by
sleep 2000
unfold f
-starting comment here
```
is parsed as
```
(by
sleep 2000
unfold f
) - (starting comment here)
```
where the new nesting will discard incrementality data. By using `byTactic`'s precedence, the
stray `-` will be flagged as an unexpected token and will not disturb the syntax tree up to it. We
do not call `byTactic` directly to avoid differences in pretty printing or behavior or error
reporting between the two branches.
-/
lookahead (setExpected [] "by") >> termParser leadPrec <|>
termParser
-- As the pretty printer ignores `lookahead`, we need a custom parenthesizer to choose the correct
-- precedence
open PrettyPrinter in
@[combinator_parenthesizer declBody] def declBody.parenthesizer : Parenthesizer :=
Parenthesizer.categoryParser.parenthesizer `term 0
def declValSimple := leading_parser
" :=" >> ppHardLineUnlessUngrouped >> termParser >> Termination.suffix >> optional Term.whereDecls
" :=" >> ppHardLineUnlessUngrouped >> declBody >> Termination.suffix >> optional Term.whereDecls
def declValEqns := leading_parser
Term.matchAltsWhereDecls
def whereStructField := leading_parser

View File

@@ -437,11 +437,12 @@ def getSyntaxNodeKinds (env : Environment) : List SyntaxNodeKind :=
def getTokenTable (env : Environment) : TokenTable :=
(parserExtension.getState env).tokens
def mkInputContext (input : String) (fileName : String) : InputContext := {
input := input,
fileName := fileName,
fileMap := input.toFileMap
}
-- Note: `crlfToLf` preserves logical line and column numbers for each character.
def mkInputContext (input : String) (fileName : String) (normalizeLineEndings := true) : InputContext :=
let input' := if normalizeLineEndings then input.crlfToLf else input
{ input := input',
fileName := fileName,
fileMap := input'.toFileMap }
def mkParserState (input : String) : ParserState :=
{ cache := initCacheForInput input }
@@ -453,7 +454,7 @@ def runParserCategory (env : Environment) (catName : Name) (input : String) (fil
let s := p.run ictx { env, options := {} } (getTokenTable env) (mkParserState input)
if !s.allErrors.isEmpty then
Except.error (s.toErrorMsg ictx)
else if input.atEnd s.pos then
else if ictx.input.atEnd s.pos then
Except.ok s.stxStack.back
else
Except.error ((s.mkError "end of input").toErrorMsg ictx)
@@ -614,15 +615,30 @@ def withOpenDeclFn (p : ParserFn) : ParserFn := fun c s =>
@[inline] def withOpenDecl : Parser Parser := withFn withOpenDeclFn
inductive ParserName
/--
Helper environment extension that gives us access to built-in aliases in pure parser functions.
-/
builtin_initialize aliasExtension : EnvExtension (NameMap ParserAliasValue)
registerEnvExtension parserAliasesRef.get
/-- Result of resolving a parser name. -/
inductive ParserResolution where
/-- Reference to a category. -/
| category (cat : Name)
/--
Reference to a parser declaration in the environment. A `(Trailing)ParserDescr` if `isDescr` is
true.
-/
| parser (decl : Name) (isDescr : Bool)
-- TODO(gabriel): add parser aliases (this is blocked on doing IO in parsers)
deriving Repr
/--
Reference to a parser alias. Note that as aliases are built-in, a corresponding declaration may
not be in the environment (yet).
-/
| alias (p : ParserAliasValue)
/-- Resolve the given parser name and return a list of candidates. -/
def resolveParserNameCore (env : Environment) (currNamespace : Name)
(openDecls : List OpenDecl) (ident : Ident) : List ParserName := Id.run do
(openDecls : List OpenDecl) (ident : Ident) : List ParserResolution := Id.run do
let .ident (val := val) (preresolved := pre) .. := ident | return []
let rec isParser (name : Name) : Option Bool :=
@@ -642,16 +658,24 @@ def resolveParserNameCore (env : Environment) (currNamespace : Name)
if isParserCategory env erased then
return [.category erased]
ResolveName.resolveGlobalName env currNamespace openDecls val |>.filterMap fun
| (name, []) => (isParser name).map fun isDescr => .parser name isDescr
| _ => none
let resolved ResolveName.resolveGlobalName env currNamespace openDecls val |>.filterMap fun
| (name, []) => (isParser name).map fun isDescr => .parser name isDescr
| _ => none
unless resolved.isEmpty do
return resolved
-- Aliases are considered global declarations and so should be tried after scope-aware resolution
if let some alias := aliasExtension.getState env |>.find? erased then
return [.alias alias]
return []
/-- Resolve the given parser name and return a list of candidates. -/
def ParserContext.resolveParserName (ctx : ParserContext) (id : Ident) : List ParserName :=
def ParserContext.resolveParserName (ctx : ParserContext) (id : Ident) : List ParserResolution :=
Parser.resolveParserNameCore ctx.env ctx.currNamespace ctx.openDecls id
/-- Resolve the given parser name and return a list of candidates. -/
def resolveParserName (id : Ident) : CoreM (List ParserName) :=
def resolveParserName (id : Ident) : CoreM (List ParserResolution) :=
return resolveParserNameCore ( getEnv) ( getCurrNamespace) ( getOpenDecls) id
def parserOfStackFn (offset : Nat) : ParserFn := fun ctx s => Id.run do
@@ -660,23 +684,27 @@ def parserOfStackFn (offset : Nat) : ParserFn := fun ctx s => Id.run do
return s.mkUnexpectedError ("failed to determine parser using syntax stack, stack is too small")
let parserName@(.ident ..) := stack.get! (stack.size - offset - 1)
| s.mkUnexpectedError ("failed to determine parser using syntax stack, the specified element on the stack is not an identifier")
match ctx.resolveParserName parserName with
| [.category cat] =>
categoryParserFn cat ctx s
| [.parser parserName _] =>
let iniSz := s.stackSize
let s := adaptUncacheableContextFn (fun ctx =>
if !internal.parseQuotWithCurrentStage.get ctx.options then
-- static quotations such as `(e) do not use the interpreter unless the above option is set,
-- so for consistency neither should dynamic quotations using this function
{ ctx with options := ctx.options.setBool `interpreter.prefer_native true }
else ctx) (evalParserConst parserName) ctx s
if !s.hasError && s.stackSize != iniSz + 1 then
s.mkUnexpectedError "expected parser to return exactly one syntax object"
else
s
| _::_::_ => s.mkUnexpectedError s!"ambiguous parser name {parserName}"
| [] => s.mkUnexpectedError s!"unknown parser {parserName}"
let iniSz := s.stackSize
let s match ctx.resolveParserName parserName with
| [.category cat] =>
categoryParserFn cat ctx s
| [.parser parserName _] =>
adaptUncacheableContextFn (fun ctx =>
if !internal.parseQuotWithCurrentStage.get ctx.options then
-- static quotations such as `(e) do not use the interpreter unless the above option is set,
-- so for consistency neither should dynamic quotations using this function
{ ctx with options := ctx.options.setBool `interpreter.prefer_native true }
else ctx) (evalParserConst parserName) ctx s
| [.alias alias] =>
match alias with
| .const p => p.fn ctx s
| _ =>
return s.mkUnexpectedError s!"parser alias {parserName}, must not take parameters"
| _::_::_ => return s.mkUnexpectedError s!"ambiguous parser name {parserName}"
| [] => return s.mkUnexpectedError s!"unknown parser {parserName}"
if !s.hasError && s.stackSize != iniSz + 1 then
return s.mkUnexpectedError "expected parser to return exactly one syntax object"
s
def parserOfStack (offset : Nat) (prec : Nat := 0) : Parser where
fn := adaptCacheableContextFn ({ · with prec }) (parserOfStackFn offset)

View File

@@ -141,7 +141,7 @@ partial def testParseModuleAux (env : Environment) (inputCtx : InputContext) (s
match parseCommand inputCtx { env := env, options := {} } state msgs with
| (stx, state, msgs) =>
if isTerminalCommand stx then
if msgs.isEmpty then
if !msgs.hasUnreported then
pure stxs
else do
msgs.forM fun msg => msg.toString >>= IO.println

View File

@@ -542,8 +542,11 @@ It is often used when building macros.
@[builtin_term_parser] def «let_tmp» := leading_parser:leadPrec
withPosition ("let_tmp " >> letDecl) >> optSemicolon termParser
def haveId := leading_parser (withAnonymousAntiquot := false)
(ppSpace >> binderIdent) <|> hygieneInfo
/- like `let_fun` but with optional name -/
def haveIdLhs := ((ppSpace >> binderIdent) <|> hygieneInfo) >> many (ppSpace >> letIdBinder) >> optType
def haveIdLhs :=
haveId >> many (ppSpace >> letIdBinder) >> optType
def haveIdDecl := leading_parser (withAnonymousAntiquot := false)
atomic (haveIdLhs >> " := ") >> termParser
def haveEqnsDecl := leading_parser (withAnonymousAntiquot := false)
@@ -786,7 +789,7 @@ def isIdent (stx : Syntax) : Bool :=
checkStackTop isIdent "expected preceding identifier" >>
checkNoWsBefore "no space before '.{'" >> ".{" >>
sepBy1 levelParser ", " >> "}"
/-- `x@e` or `x:h@e` matches the pattern `e` and binds its value to the identifier `x`.
/-- `x@e` or `x@h:e` matches the pattern `e` and binds its value to the identifier `x`.
If present, the identifier `h` is bound to a proof of `x = e`. -/
@[builtin_term_parser] def namedPattern : TrailingParser := trailing_parser
checkStackTop isIdent "expected preceding identifier" >>

View File

@@ -196,7 +196,7 @@ This option can only be set on the command line, not in the lakefile or via `set
return .pure ()
where
go (node : SnapshotTree) (st : ReportSnapshotsState) : BaseIO (Task ReportSnapshotsState) := do
if !node.element.diagnostics.msgLog.isEmpty then
if node.element.diagnostics.msgLog.hasUnreported then
let diags
if let some memorized node.element.diagnostics.interactiveDiagsRef?.bindM fun ref => do
return ( ref.get).bind (·.get? MemorizedInteractiveDiagnostics) then
@@ -705,12 +705,9 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
let initParams i.readLspRequestAs "initialize" InitializeParams
let _, param i.readLspNotificationAs "textDocument/didOpen" LeanDidOpenTextDocumentParams
let doc := param.textDocument
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
"\n", which should be enough to handle both LF and CRLF correctly.
This is because LSP always refers to characters by (line, column),
so if we get the line number correct it shouldn't matter that there
is a CR there. -/
let meta : DocumentMeta := doc.uri, doc.version, doc.text.toFileMap, param.dependencyBuildMode?.getD .always
/- Note (kmill): LSP always refers to characters by (line, column),
so converting CRLF to LF preserves line and column numbers. -/
let meta : DocumentMeta := doc.uri, doc.version, doc.text.crlfToLf.toFileMap, param.dependencyBuildMode?.getD .always
let e := e.withPrefix s!"[{param.textDocument.uri}] "
let _ IO.setStderr e
let (ctx, st) try

View File

@@ -15,22 +15,6 @@ namespace Lean.Server.FileWorker
open Snapshots
open IO
structure CancelToken where
ref : IO.Ref Bool
namespace CancelToken
def new : IO CancelToken :=
CancelToken.mk <$> IO.mkRef false
def set (tk : CancelToken) : BaseIO Unit :=
tk.ref.set true
def isSet (tk : CancelToken) : BaseIO Bool :=
tk.ref.get
end CancelToken
-- TEMP: translate from new heterogeneous snapshot tree to old homogeneous async list
private partial def mkCmdSnaps (initSnap : Language.Lean.InitialSnapshot) :
AsyncList IO.Error Snapshot := Id.run do
@@ -49,7 +33,7 @@ where
stx := cmdParsed.data.stx
mpState := cmdParsed.data.parserState
cmdState := finished.cmdState
} (match cmdParsed.next? with
} (match cmdParsed.nextCmdSnap? with
| some next => .delayed <| next.task.bind go
| none => .nil)

View File

@@ -59,6 +59,7 @@ def runCommandElabM (snap : Snapshot) (meta : DocumentMeta) (c : CommandElabM α
fileMap := meta.text,
tacticCache? := none
snap? := none
cancelTk? := none
}
c.run ctx |>.run' snap.cmdState

View File

@@ -75,7 +75,10 @@ structure DocumentMeta where
uri : Lsp.DocumentUri
/-- Version number of the document. Incremented whenever the document is edited. -/
version : Nat
/-- Current text of the document. -/
/--
Current text of the document.
It is maintained such that it is normalized using `String.crlfToLf`, which preserves logical line/column numbers.
(Note: we assume that edit operations never split or merge `\r\n` line endings.) -/
text : FileMap
/--
Controls when dependencies of the document are built on `textDocument/didOpen` notifications.
@@ -98,7 +101,11 @@ def replaceLspRange (text : FileMap) (r : Lsp.Range) (newText : String) : FileMa
let «end» := text.lspPosToUtf8Pos r.«end»
let pre := text.source.extract 0 start
let post := text.source.extract «end» text.source.endPos
(pre ++ newText ++ post).toFileMap
-- `pre` and `post` already have normalized line endings, so only `newText` needs its endings normalized.
-- Note: this assumes that editing never separates a `\r\n`.
-- If `pre` ends with `\r` and `newText` begins with `\n`, the result is potentially inaccurate.
-- If this is ever a problem, we could store a second unnormalized FileMap, edit it, and normalize it here.
(pre ++ newText.crlfToLf ++ post).toFileMap
open IO
@@ -125,7 +132,7 @@ def applyDocumentChange (oldText : FileMap) : (change : Lsp.TextDocumentContentC
| TextDocumentContentChangeEvent.rangeChange (range : Range) (newText : String) =>
replaceLspRange oldText range newText
| TextDocumentContentChangeEvent.fullChange (newText : String) =>
newText.toFileMap
newText.crlfToLf.toFileMap
/-- Returns the document contents with all changes applied. -/
def foldDocumentChanges (changes : Array Lsp.TextDocumentContentChangeEvent) (oldText : FileMap) : FileMap :=

View File

@@ -728,12 +728,9 @@ end RequestHandling
section NotificationHandling
def handleDidOpen (p : LeanDidOpenTextDocumentParams) : ServerM Unit :=
let doc := p.textDocument
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
"\n", which should be enough to handle both LF and CRLF correctly.
This is because LSP always refers to characters by (line, column),
so if we get the line number correct it shouldn't matter that there
is a CR there. -/
startFileWorker doc.uri, doc.version, doc.text.toFileMap, p.dependencyBuildMode?.getD .always
/- Note (kmill): LSP always refers to characters by (line, column),
so converting CRLF to LF preserves line and column numbers. -/
startFileWorker doc.uri, doc.version, doc.text.crlfToLf.toFileMap, p.dependencyBuildMode?.getD .always
def handleDidChange (p : DidChangeTextDocumentParams) : ServerM Unit := do
let doc := p.textDocument

View File

@@ -30,6 +30,9 @@ def SourceInfo.updateTrailing (trailing : Substring) : SourceInfo → SourceInfo
| SourceInfo.original leading pos _ endPos => SourceInfo.original leading pos trailing endPos
| info => info
def SourceInfo.getRange? (canonicalOnly := false) (info : SourceInfo) : Option String.Range :=
return ( info.getPos? canonicalOnly), ( info.getTailPos? canonicalOnly)
/-! # Syntax AST -/
inductive IsNode : Syntax Prop where
@@ -80,6 +83,34 @@ end SyntaxNode
namespace Syntax
/--
Compare syntax structures and position ranges, but not whitespace.
We generally assume that if syntax trees equal in this way generate the same elaboration output,
including positions contained in e.g. diagnostics and the info tree.
-/
partial def structRangeEq : Syntax Syntax Bool
| .missing, .missing => true
| .node info k args, .node info' k' args' =>
info.getRange? == info'.getRange? && k == k' && args.isEqv args' structRangeEq
| .atom info val, .atom info' val' => info.getRange? == info'.getRange? && val == val'
| .ident info rawVal val preresolved, .ident info' rawVal' val' preresolved' =>
info.getRange? == info'.getRange? && rawVal == rawVal' && val == val' &&
preresolved == preresolved'
| _, _ => false
/-- Like `structRangeEq` but prints trace on failure if `trace.Elab.reuse` is activated. -/
def structRangeEqWithTraceReuse (opts : Options) (stx1 stx2 : Syntax) : Bool :=
if stx1.structRangeEq stx2 then
true
else
if opts.getBool `trace.Elab.reuse then
dbg_trace "reuse stopped:
{stx1.formatStx (showInfo := true)} !=
{stx2.formatStx (showInfo := true)}"
false
else
false
def getAtomVal : Syntax String
| atom _ val => val
| _ => ""
@@ -187,13 +218,6 @@ partial def updateTrailing (trailing : Substring) : Syntax → Syntax
Syntax.node info k args
| s => s
partial def getTailWithPos : Syntax Option Syntax
| stx@(atom info _) => info.getPos?.map fun _ => stx
| stx@(ident info ..) => info.getPos?.map fun _ => stx
| node SourceInfo.none _ args => args.findSomeRev? getTailWithPos
| stx@(node ..) => stx
| _ => none
open SourceInfo in
/-- Split an `ident` into its dot-separated components while preserving source info.
Macro scopes are first erased. For example, `` `foo.bla.boo._@._hyg.4 `` ↦ `` [`foo, `bla, `boo] ``.

Some files were not shown because too many files have changed in this diff Show More