diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.vale/styles/config/ignore/terms.txt b/.vale/styles/config/ignore/terms.txt index 5707548..2dd997a 100644 --- a/.vale/styles/config/ignore/terms.txt +++ b/.vale/styles/config/ignore/terms.txt @@ -1,5 +1,6 @@ APIs Abelian +Ackermann Kleisli Packrat antiquotation @@ -49,6 +50,8 @@ enum equational extensional extensionality +fixpoint +fixpoints functor functor's functors diff --git a/Manual/BasicTypes/String/FFI.lean b/Manual/BasicTypes/String/FFI.lean index 171c1d4..7e9165b 100644 --- a/Manual/BasicTypes/String/FFI.lean +++ b/Manual/BasicTypes/String/FFI.lean @@ -20,7 +20,7 @@ tag := "string-ffi" %%% -{docstring String.csize} +{docstring Char.utf8Size} :::ffi "lean_string_object" kind := type ``` diff --git a/Manual/Meta/Monotonicity.lean b/Manual/Meta/Monotonicity.lean new file mode 100644 index 0000000..547e01f --- /dev/null +++ b/Manual/Meta/Monotonicity.lean @@ -0,0 +1,110 @@ +/- +Copyright (c) 2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: Joachim Breitner +-/ + +import Verso + +import Manual.Meta.Attribute +import Manual.Meta.Basic +import Manual.Meta.Lean +import Manual.Meta.Table + +open Lean Meta Elab +open Verso Doc Elab Manual +open SubVerso.Highlighting Highlighted + + +namespace Manual + +/-- +A table for monotonicity lemmas. Likely some of this logic can be extracted to a helper +in `Manual/Meta/Table.lean`. +-/ +private def mkInlineTabe (rows : Array (Array Term)) : TermElabM Term := do + if h : rows.size = 0 then + throwError "Expected at least one row" + else + let columns := rows[0].size + if columns = 0 then + throwError "Expected at least one column" + if rows.any (·.size != columns) then + throwError s!"Expected all rows to have same number of columns, but got {rows.map (·.size)}" + + let blocks : Array Term := + #[ ← ``(Inline.text "Theorem"), ← ``(Inline.text "Pattern") ] ++ + rows.flatten + ``(Block.other (Block.table $(quote columns) (header := true) Option.none) + #[Block.ul #[$[Verso.Doc.ListItem.mk #[Block.para #[$blocks]]],*]]) + + +section delabhelpers + +/-! +To format the monotonicy lemma patterns, I’d like to clearly mark the monotone arguments from +the other arguments. So I define two gadgets with custom delaborators. +-/ + +def monoArg := @id +def otherArg := @id + +open PrettyPrinter.Delaborator + +@[app_delab monoArg] def delabMonoArg : Delab := + PrettyPrinter.Delaborator.withOverApp 2 `(·) +@[app_delab otherArg] def delabOtherArg : Delab := + PrettyPrinter.Delaborator.withOverApp 2 `(_) + +end delabhelpers + + + +@[block_role_expander monotonicityLemmas] +def monotonicityLemmas : BlockRoleExpander + | #[], #[] => do + let names := (Meta.Monotonicity.monotoneExt.getState (← getEnv)).values + let names := names.qsort (toString · < toString ·) + + let rows : Array (Array Term) ← names.mapM fun name => do + -- Extract the target pattern + let ci ← getConstInfo name + + -- Omit the `Lean.Order` namespace, if present, to keep the table concise + let nameStr := (name.replacePrefix `Lean.Order .anonymous).getString! + let hl : Highlighted ← constTok name nameStr + let nameStx ← `(Inline.other {Inline.name with data := ToJson.toJson $(quote hl)} + #[Inline.code $(quote nameStr)]) + + let patternStx : TSyntax `term ← + forallTelescope ci.type fun _ concl => do + unless concl.isAppOfArity ``Lean.Order.monotone 5 do + throwError "Unexpected conclusion of {name}" + let f := concl.appArg! + unless f.isLambda do + throwError "Unexpected conclusion of {name}" + lambdaBoundedTelescope f 1 fun x call => do + -- Monotone arguments are the free variables applied to `x`, + -- Other arguments the other + -- This is an ad-hoc transformation and may fail in cases more complex + -- than we need right now (e.g. binders in the goal). + let call' ← Meta.transform call (pre := fun e => do + if e.isApp && e.appFn!.isFVar && e.appArg! == x[0]! then + .done <$> mkAppM ``monoArg #[e] + else if e.isFVar then + .done <$> mkAppM ``otherArg #[e] + else + pure .continue) + + let fmt ← ppExpr call' + `(Inline.code $(quote fmt.pretty)) + + pure #[nameStx, patternStx] + + let tableStx ← mkInlineTabe rows + return #[tableStx] + | _, _ => throwError "Unexpected arguments" + +-- #eval do +-- let (ss, _) ← (monotonicityLemmas #[] #[]).run {} (.init .missing) +-- logInfo (ss[0]!.raw.prettyPrint) diff --git a/Manual/RecursiveDefs.lean b/Manual/RecursiveDefs.lean index fd60f42..d43b30d 100644 --- a/Manual/RecursiveDefs.lean +++ b/Manual/RecursiveDefs.lean @@ -10,6 +10,7 @@ import Manual.Meta import Manual.RecursiveDefs.Structural import Manual.RecursiveDefs.WF +import Manual.RecursiveDefs.PartialFixpoint open Verso.Genre Manual open Lean.Elab.Tactic.GuardMsgs.WhitespaceMode @@ -30,7 +31,7 @@ Furthermore, most useful recursive functions do not threaten soundness, and infi Instead of banning recursive functions, Lean requires that each recursive function is defined safely. While elaborating recursive definitions, the Lean elaborator also produces a justification that the function being defined is safe.{margin}[The section on {ref "elaboration-results"}[the elaborator's output] in the overview of elaboration contextualizes the elaboration of recursive definitions in the overall context of the elaborator.] -There are four main kinds of recursive functions that can be defined: +There are five main kinds of recursive functions that can be defined: : Structurally recursive functions @@ -48,6 +49,15 @@ There are four main kinds of recursive functions that can be defined: Applications of functions defined via well-founded recursion are not necessarily definitionally equal to their return values, but this equality can be proved as a proposition. Even when definitional equalities exist, these functions are frequently slow to compute with because they require reducing proof terms that are often very large. +: Recursive functions as fixpoints + + Taking the function definition as an equation that specifies the behavior of the function, in certain cases the existence of a function satisfying this specification can be proven. + This strategy can apply even when the function definition is not necessarily terminating on all inputs; hence the term {tech}_partial fixpoint_. + + In particular monadic functions in certain monads (e.g. {name}`Option`) are amenable for this strategy, and additional theorems are generated by lean (partial correctness). + + As with well-founded recursion, applications of functions defined via partial fixpoint are not definitionally equal to their return values. + : Partial functions with nonempty ranges For many applications, it's not important to reason about the implementation of certain functions. @@ -78,7 +88,7 @@ As described in the {ref "elaboration-results"}[overview of the elaborator's out If there is no such clause, then the elaborator performs a search, testing each parameter to the function as a candidate for structural recursion, and attempting to find a measure with a well-founded relation that decreases at each recursive call. This section describes the rules that govern recursive functions. -After a description of mutual recursion, each of the four kinds of recursive definitions is specified, along with the tradeoffs between reasoning power and flexibility that go along with each. +After a description of mutual recursion, each of the five kinds of recursive definitions is specified, along with the tradeoffs between reasoning power and flexibility that go along with each. # Mutual Recursion %%% @@ -156,6 +166,8 @@ After the first step of elaboration, in which definitions are still recursive, a {include 0 Manual.RecursiveDefs.WF} +{include 0 Manual.RecursiveDefs.PartialFixpoint} + # Partial and Unsafe Recursive Definitions %%% tag := "partial-unsafe" diff --git a/Manual/RecursiveDefs/PartialFixpoint.lean b/Manual/RecursiveDefs/PartialFixpoint.lean new file mode 100644 index 0000000..59d9169 --- /dev/null +++ b/Manual/RecursiveDefs/PartialFixpoint.lean @@ -0,0 +1,363 @@ +/- +Copyright (c) 2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: Joachim Breitner +-/ + +import VersoManual + +import Manual.Meta +import Manual.Meta.Monotonicity + +open Manual +open Verso.Genre +open Verso.Genre.Manual +open Lean.Elab.Tactic.GuardMsgs.WhitespaceMode + +open Lean.Order + +#doc (Manual) "Partial Fixpoint Recursion" => +%%% +tag := "partial-fixpoint" +%%% + +A function definition can be understood as a request to Lean to construct a function of the given type that satisfies the given equation. +One purpose of the termination proof in {ref "structural-recursion"}[structural recursion] or {ref "well-founded-recursion"}[well-founded recursion] is to guarantee the existence and uniqueness the constructed functions. + +In some cases, the equation may not uniquely determine the function's (extensional) behavior, because it +does not terminate for all arguments in the above sense, but there still exist functions for which the defining equation holds. +In these cases, a definition by {deftech}_partial fixpoint_ may be possible. + +Even in cases where the defining equation fully describes the function's behavior and a termination proof using {ref "well-founded-recursion"}[well-founded recursion] would be possible it may simply be more convenient to define the function using partial fixpoint, to avoid a possible tedious termination proof. + +Definition by partial fixpoint is only used when explicitly requested by the user, by annotating the definition with {keywordOf Lean.Parser.Command.declaration}`partial_fixpoint`. + +There are two classes of functions for which a definition by partial fixpoint works: + +* tail-recursive functions of inhabited type, and +* monadic functions in a suitable monad, such as the {name}`Option` monad. + +Both classes are backed by the same theory and construction: least fixpoints of monotone equations in chain-complete partial orders. + +Lean supports {tech}[mutually recursive] functions to be defined by partial fixpoint. +For this, every function definition in a mutual block has to be annotated with {keywordOf Lean.Parser.Command.declaration}`partial_fixpoint`. + +:::example "Definition by Partial Fixpoint" + +The following function find the least natural number for which the predicate `p` holds. +If `p` never holds then this equation does not specify the behavior: the function `find` could return 42 in that case, and still satisfy the equation. + +```lean (keep := false) +def find (p : Nat → Bool) (i : Nat := 0) : Nat := + if p i then + i + else + find p (i + 1) +partial_fixpoint +``` + +The elaborator can prove that functions satisfying the equation exist, and defined `find` to be an arbitrary such function. +::: + +# Tail-recursive functions +%%% +tag := "partial-fixpoint-tailrec" +%%% + +Definition by partial fixpoint will succeed if the following two conditions hold: + +1. The function's type is inhabited (as with {ref "partial-unsafe"}[functions marked {keywordOf Lean.Parser.Command.declaration}`partial`]). +2. All recursive calls are in {tech}[tail position] of the function. + +A {deftech}_tail position_ of the function body is + +* the function body itself, +* the branches of a {keywordOf Lean.Parser.Term.match}`match` expression in tail position, +* the branches of an {keywordOf termIfThenElse}`if` expression in tail position, and +* the body of a {keywordOf Lean.Parser.Term.let}`let` expression in tail position. + +In particular, the {tech key:="match discriminant"}[discriminant] of a {keywordOf Lean.Parser.Term.match}`match` expression, the condition of an {keywordOf termIfThenElse}`if` expression and the arguments of functions are not tail-positions. + +:::example "Tail recursive functions" + +Because the function body itself is a tail-position, clearly looping definitions are accepted: + +```lean (keep := false) +def loop (x : Nat) : Nat := loop (x + 1) +partial_fixpoint +``` + +More useful function definitions tend to have some branching. +The following example could also be constructed using well-founded recursion with a termination proof, but may be more convenient to define using {keywordOf Lean.Parser.Command.declaration}`partial_fixpoint`, where no termination proof is needed. + +```lean (keep := false) +def Array.find (xs : Array α) (p : α → Bool) (i : Nat := 0) : Option α := + if h : i < xs.size then + if p xs[i] then + some xs[i] + else + Array.find xs p (i + 1) + else + none +partial_fixpoint +``` + +If the result of the recursive call is not just returned, but passed to another function, it is not in tail position and this definition fails. + +```lean (keep := false) (error := true) (name := nonTailPos) +def List.findIndex (xs : List α) (p : α → Bool) : Int := + match xs with + | [] => -1 + | x::ys => + if p x then + 0 + else + have r := List.findIndex ys p + if r = -1 then -1 else r + 1 +partial_fixpoint +``` +```leanOutput nonTailPos +Could not prove 'List.findIndex' to be monotone in its recursive calls: + Cannot eliminate recursive call `List.findIndex ys p` enclosed in + let_fun r := ys✝.findIndex p; + if r = -1 then -1 else r + 1 +``` + +::: + +# Monadic functions +%%% +tag := "partial-fixpoint-monadic" +%%% + + +Definition by partial fixpoint is more powerful if the function's type is monadic and the monad is an instance of {name}`Lean.Order.MonoBind`, such as {name}`Option`. +In this case, recursive call are not restricted to tail-positions, but can also be inside higher-order monadic functions such as {name}`bind` and {name}`List.mapM`. + +The set of higher-order functions for which this works is extensible (see TODO below), so no exhaustive list is given here. +The aspiration is that a monadic recursive function definition that is built using abstract monadic operations like {name}`bind`, but not open the abstraction of the monad (e.g. by matching on the {name}`Option` value), is accepted. +In particular, using {tech}[{keywordOf Lean.Parser.Term.do}`do`-notation] should work. + +:::example "Monadic functions" + +The following function implements the Ackermann function in the {name}`Option` monad, and is accepted without an (explicit or implicit) termination proof: + +```lean (keep := false) +def ack : (n m : Nat) → Option Nat + | 0, y => some (y+1) + | x+1, 0 => ack x 1 + | x+1, y+1 => do ack x (← ack (x+1) y) +partial_fixpoint +``` + +Recursive calls may also occur within higher-order functions such as {name}`List.mapM`, if they are set up appropriately, and {tech}[{keywordOf Lean.Parser.Term.do}`do`-notation]: + +```lean (keep := false) +structure Tree where cs : List Tree + +def Tree.rev (t : Tree) : Option Tree := do + Tree.mk (← t.cs.reverse.mapM (Tree.rev ·)) +partial_fixpoint + +def Tree.rev' (t : Tree) : Option Tree := do + let mut cs := [] + for c in t.cs do + cs := (← c.rev') :: cs + return Tree.mk cs +partial_fixpoint +``` + +Pattern matching on the result of the recursive call will prevent the definition by partial fixpoint to go through: + +```lean (keep := false) (error := true) (name := monoMatch) +def List.findIndex (xs : List α) (p : α → Bool) : Option Nat := + match xs with + | [] => none + | x::ys => + if p x then + some 0 + else + match List.findIndex ys p with + | none => none + | some r => some (r + 1) +partial_fixpoint +``` +```leanOutput monoMatch +Could not prove 'List.findIndex' to be monotone in its recursive calls: + Cannot eliminate recursive call `List.findIndex ys p` enclosed in + match ys✝.findIndex p with + | none => none + | some r => some (r + 1) +``` + +In this particular case, using the {name}`Functor.map` function instead of explicit pattern matching helps: + +```lean +def List.findIndex (xs : List α) (p : α → Bool) : Option Nat := + match xs with + | [] => none + | x::ys => + if p x then + some 0 + else + (· + 1) <$> List.findIndex ys p +partial_fixpoint +``` +::: + +# Partial Correctness Theorem +%%% +tag := "partial-correctness-theorem" +%%% + +In general, for functions defined by partial fixpoint we only obtain the equational theorems that prove that the function indeed satisfies the given equation, and enables proofs by rewriting. +But these do not allow reasoning about the behavior of the function on arguments for which the function specification does not terminate. + +If the monad happens to be the {name}`Option` monad, then by construction the function equals {name}`Option.none` on all function inputs for which the defining equation is not terminating. +From this fact, Lean proves a {deftech}_partial correctness theorem_ for the function which allows concluding facts from the function's result being {name}`Option.some`. + + +:::example "Partial correctness theorem" + +Recall this function from an earlier example: + +```lean +def List.findIndex (xs : List α) (p : α → Bool) : Option Nat := + match xs with + | [] => none + | x::ys => + if p x then + some 0 + else + (· + 1) <$> List.findIndex ys p +partial_fixpoint +``` + +With this function definition, Lean proves the following partial correctness theorem: + +{TODO}[using `signature` causes max recursion error] + +``` +List.findIndex.partial_correctness {α : Type _} (motive : List α → (α → Bool) → Nat → Prop) + (h : + ∀ (findIndex : List α → (α → Bool) → Option Nat), + (∀ (xs : List α) (p : α → Bool) (r : Nat), findIndex xs p = some r → motive xs p r) → + ∀ (xs : List α) (p : α → Bool) (r : Nat), + (match xs with + | [] => none + | x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys p) = + some r → + motive xs p r) + (xs : List α) (p : α → Bool) (r : Nat) : xs.findIndex p = some r → motive xs p r +``` + +We can use this theorem to prove that the resulting number is a valid index in the list and that the predicate holds for that index: + +```lean +theorem List.findIndex_implies_pred (xs : List α) (p : α → Bool) : + xs.findIndex p = some i → xs[i]?.any p := by + apply List.findIndex.partial_correctness + (motive := fun xs p i => xs[i]?.any p) + intro findIndex ih xs p r hsome + split at hsome + next => contradiction + next x ys => + split at hsome + next => + have : r = 0 := by simp_all + simp_all + next => + simp only [Option.map_eq_map, Option.map_eq_some'] at hsome + obtain ⟨r', hr, rfl⟩ := hsome + specialize ih _ _ _ hr + simpa +``` + +::: + +# Mutual Well-Founded Recursion +%%% +tag := "mutual-partial-fixpoint" +%%% + +Lean supports the definition of {tech}[mutually recursive] functions using {tech}[partial fixpoint]. +Mutual recursion may be introduced using a {tech}[mutual block], but it also results from {keywordOf Lean.Parser.Term.letrec}`let rec` expressions and {keywordOf Lean.Parser.Command.declaration}`where` blocks. +The rules for mutual well-founded recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the {ref "mutual-syntax"}[elaboration steps] for mutual groups. + +If all functions in the mutual group have the {keywordOf Lean.Parser.Command.declaration}`partial_fixpoint` clause, then this strategy is used. + +# Theory and Construction +%%% +tag := "partial-fixpoint-theory" +%%% + +The construction builds on a variant of the Knaster–Tarski theorem: In a chain-complete partial order, every monotone function has a least fixed point. + +The necessary theory is found in the `Lean.Order` namespace. +This is not meant to be a general purpose library of order theoretic results. +Instead, the definitions and theorems therein are only intended to back the +{keywordOf Lean.Parser.Command.declaration}`partial_fixpoint` feature. + +The notion of a partial order, and that of a chain-complete partial order, are represented as type classes: + +{docstring Lean.Order.PartialOrder} + +{docstring Lean.Order.CCPO} + +The fixpoint of a monotone function can be taken using {name}`fix`, which indeed constructs a fixpoint, as shown by {name}`fix_eq`, + +{docstring Lean.Order.fix} + +{docstring Lean.Order.fix_eq} + +So to construct the function, Lean first infers a suitable {name}`CCPO` instance. + +```lean (show := false) +section +universe u v +variable (α : Type u) +variable (β : α → Sort v) [∀ x, CCPO (β x)] +variable (w : α) +``` + +* If the function's result type has a dedicated instance, like {name}`Option` has with {name}`instCCPOOption`, this is used together with the instance for the function type, {name}`instCCPOPi`, to construct an instance for the whole function's type. + +* Else, if the function's type can be shown to be inhabited by a witness {lean}`w`, then the instance {name}`FlatOrder.instCCPO` for the wrapper type {lean}`FlatOrder w` is used. In this order, {lean}`w` is a least element and all other elements are incomparable. + +```lean (show := false) +end +``` + +Next, the recursive calls in the right-hand side of the function definitions are abstracted; this turns into the argument `f` of {name}`fix`. The monotonicity requirement is solved by the {tactic}`monotonicity` tactic, which applies compositional monotonicity lemmas in a syntax-driven way + +```lean (show := false) +section +set_option linter.unusedVariables false +variable {α : Sort u} {β : Sort v} [PartialOrder α] [PartialOrder β] (more : (x : α) → β) (x : α) + +local macro "…" x:term:arg "…" : term => `(more $x) +``` + +The tactic solves goals of the form {lean}`monotone (fun x => … x …)` using the following steps: + +* Applying {name}`monotone_const` when there is no dependency on {lean}`x` left. +* Splitting on {keywordOf Lean.Parser.Term.match}`match` expressions. +* Splitting on {keywordOf termIfThenElse}`if` expressions. +* Moving {keywordOf Lean.Parser.Term.let}`let` expression to the context, if the value and type do not depend on {lean}`x`. +* Zeta-reducing a {keywordOf Lean.Parser.Term.let}`let` expression when value and type do depend on {lean}`x`. +* Applying lemmas annotated with {attr}`partial_fixpoint_monotone` + +```lean (show := false) +end +``` + + +{TODO}[I wonder if this needs to be collapsible. I at some point I had it in an example, but it's not really an example. Should be this collapsible? Is there a better way than to use example?] + +{TODO}[This table probably needs some styling? Less vertical space maybe?] + +{TODO}[How can we I pretty-print these pattern expressions so that hovers work?] + +The following monotonicity lemmas are registered, and should allow recursive calls under the given higher-order functions in the arguments indicated by `·` (but not the other arguments, shown as `_`). + +{monotonicityLemmas} diff --git a/Manual/RecursiveDefs/WF.lean b/Manual/RecursiveDefs/WF.lean index e05127c..f81c5bb 100644 --- a/Manual/RecursiveDefs/WF.lean +++ b/Manual/RecursiveDefs/WF.lean @@ -412,7 +412,7 @@ In particular, it tries the following tactics and theorems: * {tactic}`simp_arith` * {tactic}`assumption` -* theorems {name}`Nat.sub_succ_lt_self`, {name}`Nat.pred_lt'`, {name}`Nat.pred_lt`, which handle common arithmetic goals +* theorems {name}`Nat.sub_succ_lt_self`, {name}`Nat.pred_lt_of_lt`, {name}`Nat.pred_lt`, which handle common arithmetic goals * {tactic}`omega` * {tactic}`array_get_dec` and {tactic}`array_mem_dec`, which prove that the size of array elements is less than the size of the array * {tactic}`sizeOf_list_dec` that the size of list elements is less than the size of the list diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6f885a8 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1736243181, + "narHash": "sha256-yaAZO4ttZ3q9/U0zFVtzpVGO6B8+DMpshnF1+0E5Kkg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8bcd7d056d69e2e6c47ddf40c2c401cb173c0d67", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d9fb84b --- /dev/null +++ b/flake.nix @@ -0,0 +1,15 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/master"; + outputs = { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in + { devShell.${system} = pkgs.stdenv.mkDerivation rec { + name = "env"; + buildInputs = with pkgs; [ + elan + python3 + ]; + };}; +} diff --git a/lake-manifest.json b/lake-manifest.json index 34a71e2..2ff79ee 100644 --- a/lake-manifest.json +++ b/lake-manifest.json @@ -5,7 +5,7 @@ "type": "git", "subDir": null, "scope": "", - "rev": "844b9a53cf7acd71e0e81a3a56608bb28215646d", + "rev": "73ee3629d01f3255b23645f56d8e587e1337a01f", "name": "subverso", "manifestFile": "lake-manifest.json", "inputRev": "main", diff --git a/lean-toolchain b/lean-toolchain index 2ffc30c..2a88805 100644 --- a/lean-toolchain +++ b/lean-toolchain @@ -1 +1 @@ -leanprover/lean4:v4.16.0-rc2 \ No newline at end of file +leanprover/lean4-nightly:nightly-2025-01-22