Skip to content

Commit c2c8384

Browse files
feat: partial_fixpoint (#253)
The manual section to go with leanprover/lean4#6355. --------- Co-authored-by: David Thrane Christiansen <david@davidchristiansen.dk>
1 parent f941fa8 commit c2c8384

8 files changed

Lines changed: 644 additions & 5 deletions

File tree

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

.vale/styles/config/ignore/terms.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ equational
5252
executable's
5353
extensional
5454
extensionality
55+
fixpoint
56+
fixpoints
5557
functor
5658
functor's
5759
functors

Manual/Meta/Monotonicity.lean

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Author: Joachim Breitner
5+
-/
6+
7+
import Verso
8+
9+
import Manual.Meta.Attribute
10+
import Manual.Meta.Basic
11+
import Manual.Meta.CustomStyle
12+
import Manual.Meta.Lean
13+
import Manual.Meta.Table
14+
15+
open Lean Meta Elab
16+
open Verso Doc Elab Manual
17+
open Verso.Genre.Manual
18+
open SubVerso.Highlighting Highlighted
19+
20+
21+
namespace Manual
22+
23+
/--
24+
A table for monotonicity lemmas. Likely some of this logic can be extracted to a helper
25+
in `Manual/Meta/Table.lean`.
26+
-/
27+
private def mkInlineTable (rows : Array (Array Term)) (tag : Option String := none) : TermElabM Term := do
28+
if h : rows.size = 0 then
29+
throwError "Expected at least one row"
30+
else
31+
let columns := rows[0].size
32+
if columns = 0 then
33+
throwError "Expected at least one column"
34+
if rows.any (·.size != columns) then
35+
throwError s!"Expected all rows to have same number of columns, but got {rows.map (·.size)}"
36+
37+
let blocks : Array Term :=
38+
#[ ← ``(Inline.text "Theorem"), ← ``(Inline.text "Pattern") ] ++
39+
rows.flatten
40+
-- The tag down here is relying on the coercion from `String` to `Tag`
41+
``(Block.other (Block.table $(quote columns) (header := true) Option.none Option.none (tag := $(quote tag)))
42+
#[Block.ul #[$[Verso.Doc.ListItem.mk #[Block.para #[$blocks]]],*]])
43+
44+
45+
section delabhelpers
46+
47+
/-!
48+
To format the monotonicy lemma patterns, I’d like to clearly mark the monotone arguments from
49+
the other arguments. So I define two gadgets with custom delaborators.
50+
-/
51+
52+
def monoArg := @id
53+
def otherArg := @id
54+
55+
open PrettyPrinter.Delaborator
56+
57+
@[app_delab monoArg] def delabMonoArg : Delab :=
58+
PrettyPrinter.Delaborator.withOverApp 2 `(·)
59+
@[app_delab otherArg] def delabOtherArg : Delab :=
60+
PrettyPrinter.Delaborator.withOverApp 2 `(_)
61+
62+
end delabhelpers
63+
64+
65+
66+
@[block_role_expander monotonicityLemmas]
67+
def monotonicityLemmas : BlockRoleExpander
68+
| #[], #[] => do
69+
let names := (Meta.Monotonicity.monotoneExt.getState (← getEnv)).values
70+
let names := names.qsort (toString · < toString ·)
71+
72+
let rows : Array (Array Term) ← names.mapM fun name => do
73+
-- Extract the target pattern
74+
let ci ← getConstInfo name
75+
76+
-- Omit the `Lean.Order` namespace, if present, to keep the table concise
77+
let nameStr := (name.replacePrefix `Lean.Order .anonymous).getString!
78+
let hl : Highlighted ← constTok name nameStr
79+
let nameStx ← `(Inline.other {Inline.name with data := ToJson.toJson $(quote hl)}
80+
#[Inline.code $(quote nameStr)])
81+
82+
let patternStx : TSyntax `term ←
83+
forallTelescope ci.type fun _ concl => do
84+
unless concl.isAppOfArity ``Lean.Order.monotone 5 do
85+
throwError "Unexpected conclusion of {name}"
86+
let f := concl.appArg!
87+
unless f.isLambda do
88+
throwError "Unexpected conclusion of {name}"
89+
lambdaBoundedTelescope f 1 fun x call => do
90+
-- Monotone arguments are the free variables applied to `x`,
91+
-- Other arguments the other
92+
-- This is an ad-hoc transformation and may fail in cases more complex
93+
-- than we need right now (e.g. binders in the goal).
94+
let call' ← Meta.transform call (pre := fun e => do
95+
if e.isApp && e.appFn!.isFVar && e.appArg! == x[0]! then
96+
.done <$> mkAppM ``monoArg #[e]
97+
else if e.isFVar then
98+
.done <$> mkAppM ``otherArg #[e]
99+
else
100+
pure .continue)
101+
102+
let hlCall ← withOptions (·.setBool `pp.tagAppFns true) do
103+
let fmt ← Lean.Widget.ppExprTagged call'
104+
renderTagged none fmt ⟨{}, false
105+
let fmt ← ppExpr call'
106+
``(Inline.other (Inline.lean $(quote hlCall)) #[(Inline.code $(quote fmt.pretty))])
107+
108+
pure #[nameStx, patternStx]
109+
110+
let tableStx ← mkInlineTable rows (tag := "--monotonicity-lemma-table")
111+
let extraCss ← `(Block.other {Block.CSS with data := $(quote css)} #[])
112+
return #[extraCss, tableStx]
113+
| _, _ => throwError "Unexpected arguments"
114+
where
115+
css := r#"
116+
table#--monotonicity-lemma-table {
117+
border-collapse: collapse;
118+
}
119+
table#--monotonicity-lemma-table th {
120+
text-align: center;
121+
}
122+
table#--monotonicity-lemma-table th, table#--monotonicity-lemma-table th p {
123+
font-family: var(--verso-structure-font-family);
124+
}
125+
table#--monotonicity-lemma-table td:first-child {
126+
padding-bottom: 0.25em;
127+
padding-top: 0.25em;
128+
padding-left: 0;
129+
padding-right: 1.5em;
130+
}
131+
"#
132+
133+
-- #eval do
134+
-- let (ss, _) ← (monotonicityLemmas #[] #[]).run {} (.init .missing)
135+
-- logInfo (ss[0]!.raw.prettyPrint)

Manual/Meta/Table.lean

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ def Alignment.htmlClass : Alignment → String
3939
| .center => "center-align"
4040
end TableConfig
4141

42-
def Block.table (columns : Nat) (header : Bool) (name : Option String) (alignment : Option TableConfig.Alignment) (tag : Option Tag := none): Block where
42+
def Block.table (columns : Nat) (header : Bool) (tag : Option String) (alignment : Option TableConfig.Alignment) (assignedTag : Option Tag := none) : Block where
4343
name := `Manual.table
44-
data := ToJson.toJson (columns, header, name, tag, alignment)
44+
data := ToJson.toJson (columns, header, tag, assignedTag, alignment)
4545

4646
structure TableConfig where
4747
/-- Name for refs -/
@@ -111,7 +111,7 @@ def table.descr : BlockDescr where
111111
| .ok (c, hdr, some x, none, align) =>
112112
let path ← (·.path) <$> read
113113
let tag ← Verso.Genre.Manual.externalTag id path x
114-
pure <| some <| Block.other {Block.table c hdr (some x) (tag := some tag) align with id := some id} contents
114+
pure <| some <| Block.other {Block.table c hdr (some x) (assignedTag := some tag) align with id := some id} contents
115115
| .ok (_, _, some _, some _, _) => pure none
116116
toTeX := none
117117
toHtml :=

Manual/RecursiveDefs.lean

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Manual.Meta
1010

1111
import Manual.RecursiveDefs.Structural
1212
import Manual.RecursiveDefs.WF
13+
import Manual.RecursiveDefs.PartialFixpoint
1314

1415
open Verso.Genre Manual
1516
open Lean.Elab.Tactic.GuardMsgs.WhitespaceMode
@@ -30,7 +31,7 @@ Furthermore, most useful recursive functions do not threaten soundness, and infi
3031
Instead of banning recursive functions, Lean requires that each recursive function is defined safely.
3132
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.]
3233

33-
There are four main kinds of recursive functions that can be defined:
34+
There are five main kinds of recursive functions that can be defined:
3435

3536
: Structurally recursive functions
3637

@@ -48,6 +49,17 @@ There are four main kinds of recursive functions that can be defined:
4849
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.
4950
Even when definitional equalities exist, these functions are frequently slow to compute with because they require reducing proof terms that are often very large.
5051

52+
: Recursive functions as partial fixpoints
53+
54+
The definition of a function can be understood as an equation that specifies its behavior.
55+
In certain cases, the existence of a function that satisfies this specification can be proven even when the recursive function does not necessarily terminate for all inputs.
56+
This strategy is even applicable in some cases where the function definition does not necessarily terminate for all inputs.
57+
These partial functions emerge as fixed points of these equations are called {tech}_partial fixpoints_.
58+
59+
In particular, any function whose return type is in certain monads (e.g. {name}`Option`) can be defined using this strategy.
60+
Lean generates additional partial correctness theorems for these monadic functions.
61+
As with well-founded recursion, applications of functions defined as partial fixpoints are not definitionally equal to their return values, but Lean generates theorems that propositionally equate the function to its unfolding and to the reduction behavior specified in its definition.
62+
5163
: Partial functions with nonempty ranges
5264

5365
For many applications, it's not important to reason about the implementation of certain functions.
@@ -66,6 +78,11 @@ There are four main kinds of recursive functions that can be defined:
6678
The replaced function may be opaque, which results in the function name having a trivial equational theory in the logic, or it may be an ordinary function, in which case the function is used in the logic.
6779
Use this feature with care: logical soundness is not at risk, but the behavior of programs written in Lean may diverge from their verified logical models if the unsafe implementation is incorrect.
6880

81+
:::TODO
82+
83+
Table providing an overview of all strategies and their properties
84+
85+
:::
6986

7087
As described in the {ref "elaboration-results"}[overview of the elaborator's output], elaboration of recursive functions proceeds in two phases:
7188
1. The definition is elaborated as if Lean's core type theory had recursive definitions.
@@ -78,7 +95,7 @@ As described in the {ref "elaboration-results"}[overview of the elaborator's out
7895
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.
7996

8097
This section describes the rules that govern recursive functions.
81-
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.
98+
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.
8299

83100
# Mutual Recursion
84101
%%%
@@ -156,6 +173,8 @@ After the first step of elaboration, in which definitions are still recursive, a
156173

157174
{include 0 Manual.RecursiveDefs.WF}
158175

176+
{include 0 Manual.RecursiveDefs.PartialFixpoint}
177+
159178
# Partial and Unsafe Recursive Definitions
160179
%%%
161180
tag := "partial-unsafe"

0 commit comments

Comments
 (0)