Commit graph

10220 commits

Author SHA1 Message Date
Sebastian Ullrich
4323507b91
fix: linter should have access to complete command message log (#8101)
This PR fixes a parallelism regression where linters that e.g. check for
errors in the command would no longer find such messages.

---------

Co-authored-by: damiano <adomani@gmail.com>
2025-04-26 11:36:21 +00:00
Sebastian Ullrich
c268602795
fix: wf preprocess of ite (#8112)
`[wf_preprocess]` expects a dsimp theorem, which in `Init` temporarily
have a simplistic syntactic representation until a more robust solution
is implemented.
2025-04-26 07:30:45 +00:00
Leonardo de Moura
60ee8c2f76
chore: broken test after update stage0 (#8110)
This is a temporary fix for `master` after update stage0 breakage.

cc @Kha @nomeata
2025-04-26 00:02:23 +00:00
Sebastian Ullrich
62c6edffef
feat: do not export theorem bodies (#8090)
This PR adjusts the experimental module system to elide theorem bodies
(i.e. proofs) from being imported into other modules.
2025-04-25 20:22:32 +00:00
Joachim Breitner
3fe195a4a9
fix: FunInd with nested well-founded recurison and late fixed parameters (#8094)
This PR fixes the generation of functional induction principles for
functions with nested nested well-founded recursion and late fixed
parameters. This is a follow-up for #7166. Fixes #8093.
2025-04-25 09:20:27 +00:00
Rob23oba
416e07a68e
fix: handle surrogate pairs correctly in Json.parse (#8080)
This PR fixes `Json.parse` to handle surrogate pairs correctly.

Closes #5445
2025-04-24 19:07:46 +00:00
Henrik Böving
406bda8807
feat: implement a Selector for async TCP (#8078)
This PR is a follow up to #8055 and implements a `Selector` for async
TCP in order to allow IO multiplexing using TCP sockets.

As we must not commit to actually fetching data from the socket buffer
this cannot be implemented by just racing on `recv?`. Instead we perform
a call to `uv_read_start` and pass an `alloc_cb` that allocates no
memory at all. According to the docs of
[`uv_alloc_cb`](https://docs.libuv.org/en/v1.x/handle.html#c.uv_alloc_cb)
this is guaranteed to give us a `UV_ENOBUFS` in the relevant callback.
Thus we can first run this "zero read" and then go into one of three
cases:
1. We get cancelled before the zero read completes, in this case just
cancel the zero read and give up.
2. The zero read completes and we loose the race for completing the
`select`, in this case just don't do anything anymore
3. The zero read completes and we win the race for completing the
`select`, in this case we perform the actual read on the socket. As we
know that data is available already (since the read callback of the zero
read is only triggered if data actually is available) we know that the
subsequent actual read should complete right away.

In this way we avoid any data loss if we loose the race.
2025-04-24 16:05:35 +00:00
Luisa Cicolini
bc032eec8d
feat: add BitVec.sdivOverflow definition and lemmas for overflow in signed and unsigned division (#7671)
This PR contains the theorem proving that signed division x.toInt /
y.toInt only overflows when `x = intMin w` and `y = allOnes w` (for `0 <
w`).
To show that this is the *only* case in which overflow happens, we refer
to overflow for negation
(`BitVec.sdivOverflow_eq_negOverflow_of_neg_one`): in fact,
`x.toInt/(allOnes w).toInt = - x.toInt`, i.e., the overflow conditions
are the same as `negOverflow` for `x`, and then reason about the signs
of the operands with the respective theorems.
These BitVec theorems themselves rely on numerous `Int.ediv_*` theorems,
that carefully set the bounds of signed division for integers.

co-authored by @bollu, @tobiasgrosser
2025-04-24 15:27:18 +00:00
Rob23oba
e2b3daf1dd
fix: simp?! and variants to do auto-unfolding (#8076)
This PR fixes `simp?!`, `simp_all?!` and `dsimp?!` to do auto-unfolding.

Closes #7927
2025-04-24 14:04:39 +00:00
Markus Himmel
68d9d14d44
chore: do not use the coercion α → Option α in Init and Std (#8085)
This PR moves the coercion `α → Option α` to the new file
`Init.Data.Option.Coe`. This file may not be imported anywhere in `Init`
or `Std`.
2025-04-24 13:35:01 +00:00
Joachim Breitner
9fbdf847bd
fix: FunInd: properly split mutual structural recursion with extra parameters (#8086)
This PR makes sure that the functional induction priciples for mutually
recursive structural functions with extra parameters are split deeply,
as expected.
2025-04-24 13:32:53 +00:00
Joachim Breitner
d38d9400d8
fix: avoid panic in functional induction principle for structural recursion (#8083)
This PR fixes #8081.
2025-04-24 11:58:29 +00:00
Markus Himmel
781c94f2cf
chore: test that there are no orphaned modules (#8082)
This PR adds a test that makes sure that there are no orphaned modules.
2025-04-24 11:55:07 +00:00
Joachim Breitner
b2ed6ac939
refactor: WF: add eq_def theorem for ._unary (#8063)
This PR adds an `foo._unary.eq_def` theorem, so that unfolding
`foo._unary` works as expected. This will help with #8019.
2025-04-24 09:59:08 +00:00
Sebastian Ullrich
51defe5935
chore: disable nondeterministic test 2025-04-24 11:30:26 +02:00
Sebastian Ullrich
c8cdb57c4b
feat: move non-essential metadata into .olean.server (#8068)
This PR ensures that for modules opted into the experimental module
system, we do not import module docstrings or declaration ranges.

Excluding declaration docstrings as well would require some more work to
make `[inherit_doc]` leave a mere reference to the other declaration
instead of copying its docstring eagerly.
2025-04-24 08:12:26 +00:00
Henrik Böving
58c7e5da94
feat: async IO multiplexing framework + implementation for timers (#8055)
This PR adds an implementation of an async IO multiplexing framework as
well as an implementation of it for the `Timer` API in order to
demonstrate it.

The main motivation is to have fair and data loss free multiplexing of
event sources.
To illustrate two situations where just naively racing two tasks that
read from an event source might be the wrong thing:
1. Suppose we are waiting on two channel reads that are continuously
being filled up. As the first channel will always be ready when we start
its receive function it will instantly resolve the race before the
second one can even try. Thus the path where we receive data from the
second channel gets starved. For this reason we want to try in random
order (for fairness) if the event sources already have data available
for us.
2. Suppose we are waiting on two socket reads and both happen to finish
at the same time. As we are now only going to select one of them to
execute further, we are going to loose data on the second one (unless
there is a user written buffering mechanism involved) as we are going to
disregard the buffer it received and do a new receive next time. For
this reason it is important to wait for an event source to be available
without committing to actually fetching some data until we know that
this particular event source is going to win the select race.

The implementation is inspired by the Oslo framework written by
@haesbaert as well as Go's
[`select`](https://go.dev/src/runtime/select.go) implementation. Given a
list of event sources to select one from it is going to:
1. Randomly shuffle them
2. Attempt to fetch data from them (in their new random order) without
blocking (for fairness). If any of them succeeds return right away.
3. If none has data available right away set all of them up to resolve a
promise. They will then race to win the right to resolve that promise.
Only the data source that wins the race is allowed to then actually
fetch data, ensuring that no other event source actually fetches data
and then fails to deliver it to the consumer.


Follow up PRs are going to add implementations of `Selector` for
`Std.Channel` as well as TCP and UDP sockets.

---------

Co-authored-by: Markus Himmel <markus@lean-fro.org>
2025-04-24 07:55:39 +00:00
Kyle Miller
42ab5dfab0
fix: have runTermElabM reset local context when types of autobound implicits contain metavariables (#7952)
This PR makes two improvements to the local context when there are
autobound implicits in `variable`s. First, the local context no longer
has two copies of every variable (the local context is rebuilt if the
types of autobound implicits have metavariables). Second, these
metavariables get names using the same algorithm used by binders that
appear in declarations (with `mkForallFVars'` instead of
`mkForallFVars`).

This removes the last use of `Term.addAutoBoundImplicits'`, which
inherently has this variable duplication issue.
2025-04-24 03:29:10 +00:00
Sebastian Ullrich
7feb583b9e
feat: enable experimental module system in Init (#8047) 2025-04-23 17:21:33 +00:00
Kim Morrison
50d18cdd75
chore: adding failing grind test (#8064)
This PR adds a failing `grind` test, showing a bug where grind is trying
to assign a metavariable incorrectly.
2025-04-23 14:47:38 +00:00
Paul Reichert
57915af218
fix: reducing Nat.pow, kernel interprets constant as Nat literal (#8060)
This PR fixes a bug in the Lean kernel. During reduction of `Nat.pow`,
the kernel did not validate that the WHNF of the first argument is a
`Nat` literal before interpreting it as an `mpz` number. This PR adds
the missing check.

### Explanation

In `type_checker::reduce_pow`, an expression was interpreted as a `Nat`
literal without previously validating that it actually was a `Nat`
literal.

We (@TwoFX and me) noticed this while fuzzing the Lean kernel with GMP
and Mimalloc disabled. Until now, the fuzzer found one crash, leading us
to this issue.

What are the consequences? If GMP is disabled, the Lean kernel will
crash on some inputs after the memory allocator returns `null`. (MPZ
tries to clone the `.const` expression in disguise of a `Nat` literal
which accidentally has a size field indicating that the number has 88
trillion `mpz` digits. This is too much for every allocator.) If GMP is
enabled, it is possible to [prove
`False`](https://live.lean-lang.org/#codez=JYWwDg9gTgLgBAGQKYEMB2AoDATJAzOAcwDoBvAVwF84AuOABQFU1gYyq5AkwjgDkV4aAXjh5yaOAH04ggHxwIYJOIDCAGxQBnDcADGKVXGDjgBACoBPRdLgWrMABZK4ABjhJVGpC6wBaH3AApcg14AHcoViNCOAADPjZIULgACnjiRLgARlcADgBKOABWZwAmGLhQiHJVbDh9DQgK6ABrAG5YtIzU/nSIJOy4fKLnAGZyyursNAByGBx8G1pefi4GKAVaYVFxAA9pORM4PeFXBycAMXqvd08bKHIkX39TRzhmpCg0dzgoJGxyHRIDRwBzAYEwRooOBocggABGHzgCJgoSQTmyAD0cqMSnU0LVMdiACw5eYEegAeQA6gBCTbLBJ9FIkUjOaiAC/JAJfkBUyWHcKDhcAARABiIwAKyQOhgEjhKGwEjA6wgeCFSx0EBAIHQtVkcGwEAwcDgqiQ8FwOgMdGQ6GIABEpeooPxgBBxEI4MRcHg0A7LXBSEbjcG0CgQF4PTEQOYHCAADRB4NwexGGDAj3EX6EaooKAuBNJ40aFB4M3menEYulguFmCWCPCZLEFBgMApYgatAhWKmOAAbQAugUm53uzFKbT+0O8jWk6aAG7uei5sPp4SD2fB+f6B70kduseme5IYip9ZTvKJuCUIM2tDEACi6jhxGUmu1+OIqhMMDfvwAsikd7Ntg2B+gYFqqJeGB+HAyjOhojjAocADi/70IYwLYGCAqmtgGBimgkrSrK8qKsqeBYGc0BICARASEgACOEgAF4fI0dAshw4gnPScLmEGYh4BANREEGGhgN+8AADytHIUB4KoVGODRdGIX0Eh4FcSyXB4DZIgJxo6PY6CEF4vbdIySSuJkl7GlASR9oACYT0UxrHsQOQZIDsKDSnA0axhgQA)
because the kernel doesn't crash on a memory allocation and instead just
happily interprets the `.const` expression as a GMP number.

Importantly, this is _not_ a flaw in Lean's type theory. It is an
implementation bug in the built-in kernel, related to the efficient
reduction of `Nat.pow`, that will be fixed with this PR; see the test
file. Because Lean's kernel is relatively small, there are third-party
kernel implementations such as `lean4lean` and `nanoda`. `lean4lean`
catches the bogus proof, and looking at its code `nanoda` will, too, but
I haven't tried it yet.
2025-04-23 13:55:20 +00:00
Sebastian Ullrich
8c8df274cf
chore: fix enabling module system (#8057) 2025-04-23 09:31:08 +00:00
Leonardo de Moura
dfa72d6c04
feat: infrastructure for computing Nullstellensatz certificates (#8059)
This PR adds infrastructure for computing Nullstellensatz certificates
in the comm ring procedure in `grind`.
2025-04-23 04:25:38 +00:00
Sebastian Ullrich
be117c4738
fix: missing traces from realizeConst (#8050)
This PR fixes missing trace messages when produced inside `realizeConst`

Fixes #8049
2025-04-22 12:23:54 +00:00
Leonardo de Moura
ff336fb63c
feat: Nullstellensatz certificates for the comm ring procedure in grind (#8043)
This PR adds `NullCert` type for representing Nullstellensatz
certificates that will be produced by the new commutative ring procedure
in `grind`.
2025-04-22 00:40:11 +00:00
Leonardo de Moura
9bdd11465c
feat: improve denoteNum (#8040)
This PR modifies `denoteNum` to avoid `intCast`. It is too verbose in
pretty printing messages.
2025-04-21 18:29:23 +00:00
Sebastian Ullrich
da82cbd3d1 feat: module header keyword for enabling module system 2025-04-21 18:40:11 +02:00
Joachim Breitner
2386a3d7c7
chore: add RISC-V ast benchmark (#8035)
This PR adds a realistic large-inductive benchmark, taken from
https://github.com/opencompl/sail-riscv-lean
2025-04-21 15:46:38 +00:00
Kyle Miller
517899da7b
feat: extract_lets and lift_lets tactics (#6432)
This PR implements tactics called `extract_lets` and `lift_lets` that
manipulate `let`/`let_fun` expressions. The `extract_lets` tactic
creates new local declarations extracted from any `let` and `let_fun`
expressions in the main goal. For top-level lets in the target, it is
like the `intros` tactic, but in general it can extract lets from deeper
subexpressions as well. The `lift_lets` tactic moves `let` and `let_fun`
expressions as far out of an expression as possible, but it does not
extract any new local declarations. The option `extract_lets +lift`
combines these behaviors.

This is a re-implementation of `extract_lets` and `lift_lets` from
mathlib. The new `extract_lets` is like doing `lift_lets; extract_lets`,
but it does not lift unextractable lets like `lift_lets`. The
`lift_lets; extract_lets` behavior is now handled by `extract_lets
+lift`. The new `lift_lets` tactic is a frontend to `extract_lets +lift`
machinery, which rather than creating new local definitions instead
represents the accumulated local declarations as top-level lets.

There are also conv tactics for both of these. The `extract_lets` has a
limitation due to the conv architecture; it can extract lets for a given
conv goal, but the local declarations don't survive outside conv. They
get zeta reduced immediately upon leaving conv.
2025-04-21 08:57:01 +00:00
Leonardo de Moura
568a1b1a81
refactor: comm ring procedure in grind (#8034)
This PR makes the following modifications to the new comm ring procedure
in `grind`
1. Adds data-structures for representing equations (and their
justifications), basis, and queue of equations to be processed.
2. Adds `RingM` helper monad.
3. Adds equation simplification main loop
2025-04-21 02:53:43 +00:00
Leonardo de Moura
a49ad77754
feat: unsat comm ring equations in grind (#8032)
This PR adds support to `grind` for detecting unsatisfiable commutative
ring equations when the ring characteristic is known. Examples:
```lean
example (x : Int) : (x + 1)*(x - 1) = x^2 → False := by
  grind +ring

example (x y : Int) : (x + 1)*(x - 1)*y + y = y*x^2 + 1 → False := by
  grind +ring

example (x : UInt8) : (x + 1)*(x - 1) = x^2 → False := by
  grind +ring

example (x y : BitVec 8) : (x + 1)*(x - 1)*y + y = y*x^2 + 1 → False := by
  grind +ring
```
2025-04-20 17:26:46 +00:00
Leonardo de Moura
de27872f3f
feat: basic CommRing support in grind (#8029)
This PR implements basic support for `CommRing` in `grind`. Terms are
already being reified and normalized. We still need to process the
equations, but `grind` can already prove simple examples such as:
```lean
open Lean.Grind in
example [CommRing α] (x : α) : (x + 1)*(x - 1) = x^2 - 1 := by
  grind +ring

open Lean.Grind in
example [CommRing α] [IsCharP α 256] (x : α) : (x + 16)*(x - 16) = x^2 := by
  grind +ring

example (x : Int) : (x + 1)*(x - 1) = x^2 - 1 := by
  grind +ring

example (x : UInt8) : (x + 16)*(x - 16) = x^2 := by
  grind +ring

example (x : Int) : (x + 1)^2 - 1 = x^2 + 2*x := by
  grind +ring

example (x : BitVec 8) : (x + 16)*(x - 16) = x^2 := by
  grind +ring

example (x : BitVec 8) : (x + 1)^2 - 1 = x^2 + 2*x := by
  grind +ring
```
2025-04-20 05:12:09 +00:00
Leonardo de Moura
876680001b
feat: add Poly.simp? (#8027)
This PR adds `Poly.simp?` and improves the function for computing
S-polynomials.
2025-04-19 20:10:00 +00:00
Leonardo de Moura
f463b62ac3
feat: S-polynomials and cleanup (#8025)
This PR simplifies the `CommRing` monomials, and adds 
1. Monomial `lcm`
2. Monomial division
3. S-polynomials
2025-04-19 04:21:04 +00:00
Joachim Breitner
02b206af9b
fix: mkAppM to typecheck at .default transparency (#7957)
This PR ensures that `mkAppM` can be used to construct terms that are
only type-correct at at default transparency, even if we are in
`withReducible` (e.g. in simp), so that simp does not stumble over
simplifying `let` expression with simplifiable type.reliable.

Here is a reproducer of the issue this solves:
```
example (a b : Nat) (h : a = b):
  (let _ : id Bool := true; a) = (let _ : Bool := true; b) := by
  simp -zeta -zetaDelta [h]
```

This fixes #7826.
2025-04-18 09:23:51 +00:00
Leonardo de Moura
27a7a0a2bd
fix: CommRing multivariate polynomials (#8016)
This PR fixes several issues in the `CommRing` multivariate polynomial
library:
1. Replaces the previous array type with the universe polymorphic
`RArray`.
2. Properly eliminates cancelled monomials.
3. Sorts monomials in decreasing order.
4. Marks the parameter `p` of the `IsCharP` class as an output
parameter.
5. Adds `LawfulBEq` instances for the types `Power`, `Mon`, and `Poly`.
2025-04-18 04:34:05 +00:00
Leonardo de Moura
807182d63e
chore: allow RArray to be universe polymorphic (#8013)
This PR ensures that `RArray` can be made universe polymorphic. We need
an update-stage0 before finalizing this modification.
2025-04-18 01:10:44 +00:00
Leonardo de Moura
96fd2f195c
feat: add debug.terminalTacticsAsSorry (#8012)
This PR adds the option `debug.terminalTacticsAsSorry`. When enabled,
terminal tactics such as `grind` and `omega` are replaced with `sorry`.
Useful for debugging and fixing bootstrapping issues.
2025-04-18 00:10:59 +00:00
Cameron Zwarich
d52b8e3cc1
fix: use lcAny in more cases of type erasure (#7990)
This PR adopts lcAny in more cases of type erasure in the new code
generator.
2025-04-16 22:53:18 +00:00
Kim Morrison
2a5373258f
chore: add grind non-determinism repro (#7978)
This PR adds a repro for a non-determinism problem in `grind`.
2025-04-16 22:36:22 +00:00
Leonardo de Moura
a3a11ffaf9
feat: revlex and grevlex monomial orders (#7986)
This PR implements reverse lexicographical and graded reverse
lexicographical orders for `CommRing` monomials.
2025-04-16 18:03:53 +00:00
Rob23oba
7cca594a4a
chore: adjust BEq classes (#7855)
This PR moves `ReflBEq` to `Init.Core` and changes `LawfulBEq` to extend
`ReflBEq`.

**BREAKING CHANGES:**
- The `refl` field of `ReflBEq` has been renamed to `rfl` to match
`LawfulBEq`
- `LawfulBEq` extends `ReflBEq`, so in particular `LawfulBEq.rfl` is no
longer valid
2025-04-16 13:24:23 +00:00
Kim Morrison
4bea52c48e
chore: failing grind test (#7981)
`propagateForallPropDown` is assuming the domain is a `Prop`
2025-04-16 07:24:53 +00:00
Markus Himmel
5a34ffb9b0
chore: upstream Nat material from mathlib (#7971)
This PR upstreams much of the material from `Mathlib/Data/Nat/Init.lean`
and `Mathlib/Data/Nat/Basic.lean`.
2025-04-16 06:55:32 +00:00
Kyle Miller
dd84829282
feat: allow omission of => ?_ in induction/cases tactics (#7830)
This PR modifies the syntax of `induction`, `cases`, and other tactics
that use `Lean.Parser.Tactic.inductionAlts`. If a case omits `=> ...`
then it is assumed to be `=> ?_`. Example:
```lean
example (p : Nat × Nat) : p.1 = p.1 := by
  cases p with | _ p1 p2
  /-
  case mk
  p1 p2 : Nat
  ⊢ (p1, p2).fst = (p1, p2).fst
  -/
```
This works with multiple cases as well. Example:
```lean
example (n : Nat) : n + 1 = 1 + n := by
  induction n with | zero | succ n ih
  /-
  case zero
  ⊢ 0 + 1 = 1 + 0
  
  case succ
  n : Nat
  ih : n + 1 = 1 + n
  ⊢ n + 1 + 1 = 1 + (n + 1)
  -/
```
The `induction n with | zero | succ n ih` is short for `induction n with
| zero | succ n ih => ?_`, which is short for `induction n with | zero
=> ?_ | succ n ih => ?_`. Note that a consequence of parsing is that
only the last alternative can omit `=>`. Any `=>`-free alternatives
before an alternative with `=>` will be a part of that alternative.

Rationale:
- In the future we may require `tacticSeq` to be indented. For
one-constructor types, this lets the rest of the tactic sequence not
need indentation.
- This is a semi-structured alternative to the `cases'`/`induction'`
tactics in mathlib.
2025-04-15 22:03:46 +00:00
Henrik Böving
712bb070f9
feat: make bv_decide work on simp normal forms of shifts (#7976)
This PR ensure that `bv_decide` can handle the simp normal form of a
shift.

Consider:
```lean
theorem test1 (b s : BitVec 5) (hb : b = 0) (hs : s ≠ 0)
  : b <<< s = 0 := by
  bv_decide
```
This works out, however:
```lean
theorem test2 (b s : BitVec 5) (hb : b = 0) (hs : s ≠ 0)
  : b <<< s = 0 := by
  simp
  bv_decide
```
this fails because the `simp` normal form adds `toNat` to the right hand
argument of the `<<<` and `bv_decide` cannot deal with shifts by
non-constant `Nat`.

Discovered by @spdskatr
2025-04-15 17:26:19 +00:00
Henrik Böving
bfb02be281
fix: bv_decide default match with as many arms as constructors (#7961)
This PR fixes a bug in bv_decide where if it was presented with a match
on an enum with as many arms as constructors but the last arm being a
default match it would (wrongly) give up on the match.
2025-04-14 14:58:13 +00:00
Henrik Böving
8e9da7a1bc
feat: wait on dedicated tasks after main is finished (#7958)
This PR ensures that after `main` is finished we still wait on dedicated
tasks instead of exiting forcefully. If users wish to violently kill
their dedicated tasks at the end of main instead they can run
`IO.Process.exit` at the end of `main` instead.
2025-04-14 11:53:54 +00:00
Kyle Miller
7c9519e60c
fix: make sure all_goals restores state on failure (#7950)
This PR modifies `all_goals` so that in recovery mode it commits changes
to the state only for those goals for which the tactic succeeds (while
preserving the new message log state). Before, we were trusting that
failing tactics left things in a reasonable state, but now we roll back
and admit the goal. The changes also fixes a bug where we were rolling
back only the metacontext state and not the tactic state, leading to an
inconsistent state (a goal list with metavariables not in the
metacontext). Closes #7883

Alternatively we could stop on the first error, however it is helpful to
see what the tactic did to each goal while interactively writing a
tactic script. There is some non-monotonicity here though since tactics
can solve for metavariables that appear in successive goals, and
conceivably a later goal succeeds only if a previous one does. Given
that the non-monotonicity is limited to recovery mode (which is for
example the RHS and not the LHS of the `<;>` combinator), we think this
is acceptable.

Another justification for the change to roll back the state on each
failure is that we need to admit goals in the failing cases. When a
tactic throws an error, we cannot assume the goal list is meaningful.
Rolling back lets us admit just the goal the tactic started with,
without needing to try to work out which new metavariables should be
admitted in the error state, allowing the tactic to continue trying the
tactic on the next goal.
2025-04-14 04:16:28 +00:00
Leonardo de Moura
4e1dbe1ae8
chore: add [grind ext] funext (#7951)
Co-authored-by: Kim Morrison <kim@tqft.net>
2025-04-14 02:52:44 +00:00