Skip to content

Commit cd0be38

Browse files
authored
feat: elidible subterms (#3201)
This PR adds two new delaboration settings: `pp.deepTerms : Bool` (default: `true`) and `pp.deepTerms.threshold : Nat` (default: `20`). Setting `pp.deepTerms` to `false` will make the delaborator terminate early after `pp.deepTerms.threshold` layers of recursion and replace the omitted subterm with the symbol `⋯` if the subterm is deeper than `pp.deepTerms.threshold / 4` (i.e. it is not shallow). To display the omitted subterm in the InfoView, `⋯` can be clicked to open a popup with the delaborated subterm. <details> <summary>InfoView with pp.deepTerms set to false (click to show image)</summary> ![image](https://github.com/leanprover/lean4/assets/10852073/f6df8b2c-d769-41c8-821e-efd0af23ccfa) </details> ### Implementation - The delaborator is adjusted to use the new configuration settings and terminate early if the threshold is exceeded and the corresponding term to omit is shallow. - To be able to distinguish `⋯` from regular terms, a new constructor `Lean.Elab.Info.ofOmissionInfo` is added to `Lean.Elab.Info` that takes a value of a new type `Lean.Elab.OmissionInfo`. - `ofOmissionInfo` is needed in `Lean.Widget.makePopup` for the `Lean.Widget.InteractiveDiagnostics.infoToInteractive` RPC procedure that is used to display popups when clicking on terms in the InfoView. It ensures that the expansion of an omitted subterm is delaborated using `explicit := false`, which is typically set to `true` in popups for regular terms. - Several `Info` widget utility functions are adjusted to support `ofOmissionInfo`. - The list delaborator is adjusted with special support for `⋯` so that long lists `[x₁, ..., xₖ, ..., xₙ]` are shortened to `[x₁, ..., xₖ, ⋯]`.
1 parent 578a230 commit cd0be38

File tree

10 files changed

+180
-24
lines changed

10 files changed

+180
-24
lines changed

RELEASES.md

+2
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ example : x + foo 2 = 12 + x := by
196196

197197
* Add language server support for [call hierarchy requests](https://www.youtube.com/watch?v=r5LA7ivUb2c) ([PR #3082](https://github.com/leanprover/lean4/pull/3082)). The change to the .ilean format in this PR means that projects must be fully rebuilt once in order to generate .ilean files with the new format before features like "find references" work correctly again.
198198

199+
* Add pretty printer settings to omit deeply nested terms (`pp.deepTerms false` and `pp.deepTerms.threshold`) ([PR #3201](https://github.com/leanprover/lean4/pull/3201))
200+
199201

200202
v4.5.0
201203
---------

src/Init/NotationExtra.lean

+21-3
Original file line numberDiff line numberDiff line change
@@ -170,16 +170,34 @@ See [Theorem Proving in Lean 4][tpil4] for more information.
170170
-/
171171
syntax (name := calcTactic) "calc" calcSteps : tactic
172172

173+
/--
174+
Denotes a term that was omitted by the pretty printer.
175+
This is only used for pretty printing, and it cannot be elaborated.
176+
The presence of `⋯` is controlled by the `pp.deepTerms` and `pp.deepTerms.threshold`
177+
options.
178+
-/
179+
syntax "⋯" : term
180+
181+
macro_rules | `(⋯) => Macro.throwError "\
182+
Error: The '⋯' token is used by the pretty printer to indicate omitted terms, \
183+
and it cannot be elaborated. \
184+
Its presence in pretty printing output is controlled by the 'pp.deepTerms' and \
185+
`pp.deepTerms.threshold` options."
186+
173187
@[app_unexpander Unit.unit] def unexpandUnit : Lean.PrettyPrinter.Unexpander
174188
| `($(_)) => `(())
175189

176190
@[app_unexpander List.nil] def unexpandListNil : Lean.PrettyPrinter.Unexpander
177191
| `($(_)) => `([])
178192

179193
@[app_unexpander List.cons] def unexpandListCons : Lean.PrettyPrinter.Unexpander
180-
| `($(_) $x []) => `([$x])
181-
| `($(_) $x [$xs,*]) => `([$x, $xs,*])
182-
| _ => throw ()
194+
| `($(_) $x $tail) =>
195+
match tail with
196+
| `([]) => `([$x])
197+
| `([$xs,*]) => `([$x, $xs,*])
198+
| `(⋯) => `([$x, $tail]) -- Unexpands to `[x, y, z, ⋯]` for `⋯ : List α`
199+
| _ => throw ()
200+
| _ => throw ()
183201

184202
@[app_unexpander List.toArray] def unexpandListToArray : Lean.PrettyPrinter.Unexpander
185203
| `($(_) [$xs,*]) => `(#[$xs,*])

src/Lean/Elab/InfoTree/Main.lean

+5
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ def FVarAliasInfo.format (info : FVarAliasInfo) : Format :=
171171
def FieldRedeclInfo.format (ctx : ContextInfo) (info : FieldRedeclInfo) : Format :=
172172
f!"FieldRedecl @ {formatStxRange ctx info.stx}"
173173

174+
def OmissionInfo.format (ctx : ContextInfo) (info : OmissionInfo) : IO Format := do
175+
return f!"Omission @ {← TermInfo.format ctx info.toTermInfo}"
176+
174177
def Info.format (ctx : ContextInfo) : Info → IO Format
175178
| ofTacticInfo i => i.format ctx
176179
| ofTermInfo i => i.format ctx
@@ -183,6 +186,7 @@ def Info.format (ctx : ContextInfo) : Info → IO Format
183186
| ofCustomInfo i => pure <| Std.ToFormat.format i
184187
| ofFVarAliasInfo i => pure <| i.format
185188
| ofFieldRedeclInfo i => pure <| i.format ctx
189+
| ofOmissionInfo i => i.format ctx
186190

187191
def Info.toElabInfo? : Info → Option ElabInfo
188192
| ofTacticInfo i => some i.toElabInfo
@@ -196,6 +200,7 @@ def Info.toElabInfo? : Info → Option ElabInfo
196200
| ofCustomInfo _ => none
197201
| ofFVarAliasInfo _ => none
198202
| ofFieldRedeclInfo _ => none
203+
| ofOmissionInfo i => some i.toElabInfo
199204

200205
/--
201206
Helper function for propagating the tactic metavariable context to its children nodes.

src/Lean/Elab/InfoTree/Types.lean

+10
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ structure Bar extends Foo :=
154154
structure FieldRedeclInfo where
155155
stx : Syntax
156156

157+
/--
158+
Denotes information for the term `⋯` that is emitted by the delaborator when omitting a term
159+
due to `pp.deepTerms false`. Omission needs to be treated differently from regular terms because
160+
it has to be delaborated differently in `Lean.Widget.InteractiveDiagnostics.infoToInteractive`:
161+
Regular terms are delaborated explicitly, whereas omitted terms are simply to be expanded with
162+
regular delaboration settings.
163+
-/
164+
structure OmissionInfo extends TermInfo
165+
157166
/-- Header information for a node in `InfoTree`. -/
158167
inductive Info where
159168
| ofTacticInfo (i : TacticInfo)
@@ -167,6 +176,7 @@ inductive Info where
167176
| ofCustomInfo (i : CustomInfo)
168177
| ofFVarAliasInfo (i : FVarAliasInfo)
169178
| ofFieldRedeclInfo (i : FieldRedeclInfo)
179+
| ofOmissionInfo (i : OmissionInfo)
170180
deriving Inhabited
171181

172182
/-- The InfoTree is a structure that is generated during elaboration and used

src/Lean/PrettyPrinter/Delaborator/Basic.lean

+64-8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ structure Context where
4040
openDecls : List OpenDecl
4141
inPattern : Bool := false -- true when delaborating `match` patterns
4242
subExpr : SubExpr
43+
/-- Current recursion depth during delaboration. Used by the `pp.deepTerms false` option. -/
44+
depth : Nat := 0
4345

4446
structure State where
4547
/-- We attach `Elab.Info` at various locations in the `Syntax` output in order to convey
@@ -81,8 +83,10 @@ instance (priority := low) : MonadWithReaderOf SubExpr DelabM where
8183

8284
instance (priority := low) : MonadStateOf SubExpr.HoleIterator DelabM where
8385
get := State.holeIter <$> get
84-
set iter := modify fun ⟨infos, _⟩ => ⟨infos, iter⟩
85-
modifyGet f := modifyGet fun ⟨infos, iter⟩ => let (ret, iter') := f iter; (ret, ⟨infos, iter'⟩)
86+
set iter := modify fun s => { s with holeIter := iter }
87+
modifyGet f := modifyGet fun s =>
88+
let (ret, holeIter') := f s.holeIter
89+
(ret, { s with holeIter := holeIter' })
8690

8791
-- Macro scopes in the delaborator output are ultimately ignored by the pretty printer,
8892
-- so give a trivial implementation.
@@ -147,7 +151,7 @@ def getOptionsAtCurrPos : DelabM Options := do
147151
return opts
148152

149153
/-- Evaluate option accessor, using subterm-specific options if set. -/
150-
def getPPOption (opt : Options → Bool) : DelabM Bool := do
154+
def getPPOption (opt : Options → α) : DelabM α := do
151155
return opt (← getOptionsAtCurrPos)
152156

153157
def whenPPOption (opt : Options → Bool) (d : Delab) : Delab := do
@@ -203,10 +207,10 @@ def withBindingBodyUnusedName {α} (d : Syntax → DelabM α) : DelabM α := do
203207
liftM x
204208

205209
def addTermInfo (pos : Pos) (stx : Syntax) (e : Expr) (isBinder : Bool := false) : DelabM Unit := do
206-
let info ← mkTermInfo stx e isBinder
210+
let info := Info.ofTermInfo <| ← mkTermInfo stx e isBinder
207211
modify fun s => { s with infos := s.infos.insert pos info }
208212
where
209-
mkTermInfo stx e isBinder := return Info.ofTermInfo {
213+
mkTermInfo stx e isBinder := return {
210214
elaborator := `Delab,
211215
stx := stx,
212216
lctx := (← getLCtx),
@@ -216,22 +220,71 @@ where
216220
}
217221

218222
def addFieldInfo (pos : Pos) (projName fieldName : Name) (stx : Syntax) (val : Expr) : DelabM Unit := do
219-
let info ← mkFieldInfo projName fieldName stx val
223+
let info := Info.ofFieldInfo <| ← mkFieldInfo projName fieldName stx val
220224
modify fun s => { s with infos := s.infos.insert pos info }
221225
where
222-
mkFieldInfo projName fieldName stx val := return Info.ofFieldInfo {
226+
mkFieldInfo projName fieldName stx val := return {
223227
projName := projName,
224228
fieldName := fieldName,
225229
lctx := (← getLCtx),
226230
val := val,
227231
stx := stx
228232
}
229233

234+
def addOmissionInfo (pos : Pos) (stx : Syntax) (e : Expr) : DelabM Unit := do
235+
let info := Info.ofOmissionInfo <| ← mkOmissionInfo stx e
236+
modify fun s => { s with infos := s.infos.insert pos info }
237+
where
238+
mkOmissionInfo stx e := return {
239+
toTermInfo := ← addTermInfo.mkTermInfo stx e (isBinder := false)
240+
}
241+
242+
/--
243+
Runs the delaborator `act` with increased depth.
244+
The depth is used when `pp.deepTerms` is `false` to determine what is a deep term.
245+
See also `Lean.PrettyPrinter.Delaborator.Context.depth`.
246+
-/
247+
def withIncDepth (act : DelabM α) : DelabM α := fun ctx =>
248+
act { ctx with depth := ctx.depth + 1 }
249+
250+
/--
251+
Returns `true` if, at the current depth, we should omit the term and use `⋯` rather than
252+
delaborating it. This function can only return `true` if `pp.deepTerms` is set to `false`.
253+
It also contains a heuristic to allow "shallow terms" to be delaborated, even if they appear deep in
254+
an expression, which prevents terms such as atomic expressions or `OfNat.ofNat` literals from being
255+
delaborated as `⋯`.
256+
-/
257+
def shouldOmitExpr (e : Expr) : DelabM Bool := do
258+
if ← getPPOption getPPDeepTerms then
259+
return false
260+
261+
let depth := (← read).depth
262+
let depthThreshold ← getPPOption getPPDeepTermsThreshold
263+
let approxDepth := e.approxDepth.toNat
264+
let depthExcess := depth - depthThreshold
265+
266+
let isMaxedOutApproxDepth := approxDepth >= 255
267+
let isShallowExpression :=
268+
!isMaxedOutApproxDepth && approxDepth <= depthThreshold/4 - depthExcess
269+
270+
return depthExcess > 0 && !isShallowExpression
271+
230272
def annotateTermInfo (stx : Term) : Delab := do
231273
let stx ← annotateCurPos stx
232274
addTermInfo (← getPos) stx (← getExpr)
233275
pure stx
234276

277+
278+
/--
279+
Delaborates the current expression as `⋯` and attaches `Elab.OmissionInfo`, which influences how the
280+
subterm omitted by `⋯` is delaborated when hovered over.
281+
-/
282+
def omission : Delab := do
283+
let stx ← `(⋯)
284+
let stx ← annotateCurPos stx
285+
addOmissionInfo (← getPos) stx (← getExpr)
286+
pure stx
287+
235288
partial def delabFor : Name → Delab
236289
| Name.anonymous => failure
237290
| k =>
@@ -243,6 +296,9 @@ partial def delab : Delab := do
243296
checkSystem "delab"
244297
let e ← getExpr
245298

299+
if ← shouldOmitExpr e then
300+
return ← omission
301+
246302
-- no need to hide atomic proofs
247303
if ← pure !e.isAtomic <&&> pure !(← getPPOption getPPProofs) <&&> (try Meta.isProof e catch _ => pure false) then
248304
if ← getPPOption getPPProofsWithType then
@@ -251,7 +307,7 @@ partial def delab : Delab := do
251307
else
252308
return ← annotateTermInfo (← ``(_))
253309
let k ← getExprKind
254-
let stx ← delabFor k <|> (liftM $ show MetaM _ from throwError "don't know how to delaborate '{k}'")
310+
let stx ← withIncDepth <| delabFor k <|> (liftM $ show MetaM _ from throwError "don't know how to delaborate '{k}'")
255311
if ← getPPOption getPPAnalyzeTypeAscriptions <&&> getPPOption getPPAnalysisNeedsType <&&> pure !e.isMData then
256312
let typeStx ← withType delab
257313
`(($stx : $typeStx)) >>= annotateCurPos

src/Lean/PrettyPrinter/Delaborator/Options.lean

+12
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ register_builtin_option pp.instanceTypes : Bool := {
125125
group := "pp"
126126
descr := "(pretty printer) when printing explicit applications, show the types of inst-implicit arguments"
127127
}
128+
register_builtin_option pp.deepTerms : Bool := {
129+
defValue := true
130+
group := "pp"
131+
descr := "(pretty printer) display deeply nested terms, replacing them with `⋯` if set to false"
132+
}
133+
register_builtin_option pp.deepTerms.threshold : Nat := {
134+
defValue := 20
135+
group := "pp"
136+
descr := "(pretty printer) when `pp.deepTerms` is false, the depth at which terms start being replaced with `⋯`"
137+
}
128138
register_builtin_option pp.motives.pi : Bool := {
129139
defValue := true
130140
group := "pp"
@@ -205,5 +215,7 @@ def getPPMotivesNonConst (o : Options) : Bool := o.get pp.motives.nonConst.name
205215
def getPPMotivesAll (o : Options) : Bool := o.get pp.motives.all.name pp.motives.all.defValue
206216
def getPPInstances (o : Options) : Bool := o.get pp.instances.name pp.instances.defValue
207217
def getPPInstanceTypes (o : Options) : Bool := o.get pp.instanceTypes.name pp.instanceTypes.defValue
218+
def getPPDeepTerms (o : Options) : Bool := o.get pp.deepTerms.name pp.deepTerms.defValue
219+
def getPPDeepTermsThreshold (o : Options) : Nat := o.get pp.deepTerms.threshold.name pp.deepTerms.threshold.defValue
208220

209221
end Lean

src/Lean/Server/FileWorker/WidgetRequests.lean

+12-5
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,25 @@ def makePopup : WithRpcRef InfoWithCtx → RequestM (RequestTask InfoPopup)
5353
| none => pure none
5454
let exprExplicit? ← match i.info with
5555
| Elab.Info.ofTermInfo ti =>
56-
let ti ← ppExprTagged ti.expr (explicit := true)
57-
-- remove top-level expression highlight
58-
pure <| some <| match ti with
59-
| .tag _ tt => tt
60-
| tt => tt
56+
pure <| some <| ← ppExprTaggedWithoutTopLevelHighlight ti.expr (explicit := true)
57+
| Elab.Info.ofOmissionInfo { toTermInfo := ti } =>
58+
-- Omitted terms are simply to be expanded, not printed explicitly.
59+
-- Keep the top-level tag so that users can also see the explicit version
60+
-- of the omitted term.
61+
pure <| some <| ← ppExprTagged ti.expr (explicit := false)
6162
| Elab.Info.ofFieldInfo fi => pure <| some <| TaggedText.text fi.fieldName.toString
6263
| _ => pure none
6364
return {
6465
type := type?
6566
exprExplicit := exprExplicit?
6667
doc := ← i.info.docString? : InfoPopup
6768
}
69+
where
70+
ppExprTaggedWithoutTopLevelHighlight (e : Expr) (explicit : Bool) : MetaM CodeWithInfos := do
71+
let pp ← ppExprTagged e (explicit := explicit)
72+
return match pp with
73+
| .tag _ tt => tt
74+
| tt => tt
6875

6976
builtin_initialize
7077
registerBuiltinRpcProcedure

src/Lean/Server/InfoUtils.lean

+14-8
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ def Info.stx : Info → Syntax
112112
| ofUserWidgetInfo i => i.stx
113113
| ofFVarAliasInfo _ => .missing
114114
| ofFieldRedeclInfo i => i.stx
115+
| ofOmissionInfo i => i.stx
115116

116117
def Info.lctx : Info → LocalContext
117-
| Info.ofTermInfo i => i.lctx
118-
| Info.ofFieldInfo i => i.lctx
119-
| _ => LocalContext.empty
118+
| Info.ofTermInfo i => i.lctx
119+
| Info.ofFieldInfo i => i.lctx
120+
| Info.ofOmissionInfo i => i.lctx
121+
| _ => LocalContext.empty
120122

121123
def Info.pos? (i : Info) : Option String.Pos :=
122124
i.stx.getPos? (canonicalOnly := true)
@@ -210,14 +212,15 @@ partial def InfoTree.hoverableInfoAt? (t : InfoTree) (hoverPos : String.Pos) (in
210212

211213
def Info.type? (i : Info) : MetaM (Option Expr) :=
212214
match i with
213-
| Info.ofTermInfo ti => Meta.inferType ti.expr
214-
| Info.ofFieldInfo fi => Meta.inferType fi.val
215+
| Info.ofTermInfo ti => Meta.inferType ti.expr
216+
| Info.ofFieldInfo fi => Meta.inferType fi.val
217+
| Info.ofOmissionInfo oi => Meta.inferType oi.expr
215218
| _ => return none
216219

217220
def Info.docString? (i : Info) : MetaM (Option String) := do
218221
let env ← getEnv
219222
match i with
220-
| Info.ofTermInfo ti =>
223+
| .ofTermInfo ti =>
221224
if let some n := ti.expr.constName? then
222225
return ← findDocString? env n
223226
| .ofFieldInfo fi => return ← findDocString? env fi.projName
@@ -227,6 +230,7 @@ def Info.docString? (i : Info) : MetaM (Option String) := do
227230
if let some decl := (← getOptionDecls).find? oi.optionName then
228231
return decl.descr
229232
return none
233+
| .ofOmissionInfo _ => return none -- Do not display the docstring of ⋯ for omitted terms
230234
| _ => pure ()
231235
if let some ei := i.toElabInfo? then
232236
return ← findDocString? env ei.stx.getKind <||> findDocString? env ei.elaborator
@@ -370,12 +374,14 @@ partial def InfoTree.hasSorry : InfoTree → IO Bool :=
370374
where go ci?
371375
| .context ci t => go (ci.mergeIntoOuter? ci?) t
372376
| .node i cs =>
373-
if let (some ci, .ofTermInfo ti) := (ci?, i) then do
377+
match ci?, i with
378+
| some ci, .ofTermInfo ti
379+
| some ci, .ofOmissionInfo { toTermInfo := ti } => do
374380
let expr ← ti.runMetaM ci (instantiateMVars ti.expr)
375381
return expr.hasSorry
376382
-- we assume that `cs` are subterms of `ti.expr` and
377383
-- thus do not have to be checked as well
378-
else
384+
| _, _ =>
379385
cs.anyM (go ci?)
380386
| _ => return false
381387

tests/lean/run/deepTerms.lean

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/-!
2+
# Testing the `pp.deepTerms false` option
3+
4+
Implemented in PR #3201.
5+
-/
6+
7+
set_option pp.deepTerms false
8+
set_option pp.deepTerms.threshold 8
9+
10+
/-!
11+
Subterms of terms with depth <= pp.deepTerms.threshold are not omitted
12+
-/
13+
#check Nat.zero.succ.succ.succ.succ.succ.succ.succ.succ
14+
15+
/-!
16+
Shallow subterms (subterms with depth <= pp.deepTerms.threshold/4) of terms with
17+
depth > pp.deepTerms.threshold are not omitted
18+
-/
19+
#check Nat.zero.succ.succ.succ.succ.succ.succ.succ.succ.succ.succ
20+
21+
/-!
22+
Deep subterms of terms with depth > pp.deepTerms.threshold are omitted
23+
-/
24+
#check Nat.zero.succ.succ.succ.succ.succ.succ.succ.succ.succ.succ.succ
25+
26+
/-!
27+
Nothing is omitted in short lists
28+
-/
29+
#check [1, 2, 3, 4, 5, 6, 7, 8]
30+
31+
/-!
32+
In longer lists, the tail is omitted
33+
-/
34+
#check [1, 2, 3, 4, 5, 6, 7, 8, 9]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ Nat.zero))))))) : Nat
2+
Nat.succ
3+
(Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ Nat.zero))))))))) : Nat
4+
Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ ⋯)))))))) : Nat
5+
[1, 2, 3, 4, 5, 6, 7, 8] : List Nat
6+
[1, 2, 3, 4, 5, 6, 7, 8, ⋯] : List Nat

0 commit comments

Comments
 (0)