This PR facilitates augmenting the context of an `InfoTree` with *partial* contexts while elaborating a command. Using partial contexts, this PR also adds support for tracking the parent declaration name of a term in the `InfoTree`. The parent declaration name is needed to compute the call hierarchy in #3082. Specifically, the `Lean.Elab.InfoTree.context` constructor is refactored to take a value of the new type `Lean.Elab.PartialContextInfo` instead of a `Lean.Elab.ContextInfo`, which now refers to a full `InfoTree` context. The `PartialContextInfo` is then merged into a `ContextInfo` while traversing the tree using `Lean.Elab.PartialContextInfo.mergeIntoOuter?`. The partial context after executing `liftTermElabM` is stored in values of a new type `Lean.Elab.CommandContextInfo`. As a result of this, `Lean.Elab.ContextInfo.save` moves to `Lean.Elab.CommandContextInfo.save`. For obtaining the parent declaration for a term, a new typeclass `MonadParentDecl` is introduced to save the parent declaration in `Lean.Elab.withSaveParentDeclInfoContext`. `Lean.Elab.Term.withDeclName x` now calls `withSaveParentDeclInfoContext x` to save the declaration name. ### Migration **The changes to the `InfoTree.context` constructor break backwards compatibility with all downstream users that traverse the `InfoTree` manually instead of going through the functions in `InfoUtils.lean`.** To fix this, you can merge the outer `ContextInfo` in a traversal with the `PartialContextInfo` of an `InfoTree.context` node using `PartialContextInfo.mergeIntoOuter?`. See e.g. `Lean.Elab.InfoTree.foldInfo` for an example: ```lean partial def InfoTree.foldInfo (f : ContextInfo → Info → α → α) (init : α) : InfoTree → α := go none init where go ctx? a | context ctx t => go (ctx.mergeIntoOuter? ctx?) a t | node i ts => let a := match ctx? with | none => a | some ctx => f ctx i a ts.foldl (init := a) (go <| i.updateContext? ctx?) | _ => a ``` Downstream users that manually save `InfoTree`s may need to adjust calls to `ContextInfo.save` to use `CommandContextInfo.save` instead and potentially wrap their `CommandContextInfo` in a `PartialContextInfo.commandCtx` constructor when storing it in an `InfoTree` or `ContextInfo.mk` when creating a full context. ### Motivation As of now, `ContextInfo`s are always *full* contexts, constructed as if they were always created in `liftTermElabM` after running the `TermElabM` action. This is not strictly true; we already create `ContextInfo`s in several places other than `liftTermElabM` and work around the limitation that `ContextInfo`s are always full contexts in certain places (e.g. `Info.updateContext?` is a crux that we need because we can't always create partial contexts at the term-level), but it has mostly worked out so far. Note that one must be very careful when saving a `ContextInfo` in places other than `liftTermElabM` because the context may not be as complete as we would like (e.g. it may lack meta-variable assignments, potentially leading to a language server panic). Unfortunately, the parent declaration of a term is another example of a context that cannot be provided in `liftTermElabM`: The parent declaration is usually set via `withDeclName`, which itself lives in `TermElabM`. So by the time we are trying to save the full `ContextInfo`, the declaration name is already gone. There is no easy fix for this like in the other cases where we would really just like to augment the context with an extra field. The refactor that we decided on to resolve the issue is to refactor the `InfoTree` to take a `PartialContextInfo` instead of a `ContextInfo` and have code that traverses the `InfoTree` merge inner contexts with outer contexts to produce a full `ContextInfo` value. ### Bumps for downstream projects - `lean-pr-testing-3159` branch at Std, not yet opened as a PR - `lean-pr-testing-3159` branch at Mathlib, not yet opened as a PR - https://github.com/leanprover/LeanInk/pull/57 - https://github.com/hargoniX/LeanInk/pull/1 - https://github.com/tydeu/lean4-alloy/pull/7 - https://github.com/leanprover-community/repl/pull/29 --------- Co-authored-by: Sebastian Ullrich <sebasti@nullri.ch>
200 lines
8.2 KiB
Text
200 lines
8.2 KiB
Text
/-
|
||
Copyright (c) 2021 Microsoft Corporation. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
|
||
Authors: Wojciech Nawrocki
|
||
-/
|
||
import Lean.Meta.PPGoal
|
||
import Lean.Widget.InteractiveCode
|
||
import Lean.Data.Lsp.Extra
|
||
|
||
/-! Functionality related to tactic-mode and term-mode goals with embedded `CodeWithInfos`. -/
|
||
|
||
namespace Lean.Widget
|
||
open Server
|
||
|
||
/-- In the infoview, if multiple hypotheses `h₁`, `h₂` have the same type `α`, they are rendered
|
||
as `h₁ h₂ : α`. We call this a 'hypothesis bundle'. We use `none` instead of `some false` for
|
||
booleans to save space in the json encoding. -/
|
||
structure InteractiveHypothesisBundle where
|
||
/-- The user-friendly name for each hypothesis.
|
||
Note that these are not `Name`s: they are pretty-printed
|
||
and do not remember the macro scopes. -/
|
||
names : Array String
|
||
/-- The ids for each variable. Should have the same length as `names`. -/
|
||
fvarIds : Array FVarId
|
||
type : CodeWithInfos
|
||
/-- The value, in the case the hypothesis is a `let`-binder. -/
|
||
val? : Option CodeWithInfos := none
|
||
/-- The hypothesis is a typeclass instance. -/
|
||
isInstance? : Option Bool := none
|
||
/-- The hypothesis is a type. -/
|
||
isType? : Option Bool := none
|
||
/-- If true, the hypothesis was not present on the previous tactic state.
|
||
Only present in tactic-mode goals. -/
|
||
isInserted? : Option Bool := none
|
||
/-- If true, the hypothesis will be removed in the next tactic state.
|
||
Only present in tactic-mode goals. -/
|
||
isRemoved? : Option Bool := none
|
||
deriving Inhabited, RpcEncodable
|
||
|
||
/-- The shared parts of interactive term-mode and tactic-mode goals. -/
|
||
structure InteractiveGoalCore where
|
||
hyps : Array InteractiveHypothesisBundle
|
||
/-- The target type. -/
|
||
type : CodeWithInfos
|
||
/-- Metavariable context that the goal is well-typed in. -/
|
||
ctx : WithRpcRef Elab.ContextInfo
|
||
|
||
/-- An interactive tactic-mode goal. -/
|
||
structure InteractiveGoal extends InteractiveGoalCore where
|
||
/-- The name `foo` in `case foo`, if any. -/
|
||
userName? : Option String
|
||
/-- The symbol to display before the target type. Usually `⊢ ` but `conv` goals use `∣ `
|
||
and it could be extended. -/
|
||
goalPrefix : String
|
||
/-- Identifies the goal (ie with the unique name of the MVar that it is a goal for.) -/
|
||
mvarId : MVarId
|
||
/-- If true, the goal was not present on the previous tactic state. -/
|
||
isInserted? : Option Bool := none
|
||
/-- If true, the goal will be removed on the next tactic state. -/
|
||
isRemoved? : Option Bool := none
|
||
deriving RpcEncodable
|
||
|
||
/-- An interactive term-mode goal. -/
|
||
structure InteractiveTermGoal extends InteractiveGoalCore where
|
||
/-- Syntactic range of the term. -/
|
||
range : Lsp.Range
|
||
/-- Information about the term whose type is the term-mode goal. -/
|
||
term : WithRpcRef Elab.TermInfo
|
||
deriving RpcEncodable
|
||
|
||
def InteractiveGoalCore.pretty (g : InteractiveGoalCore) (userName? : Option String)
|
||
(goalPrefix : String) : Format := Id.run do
|
||
let indent := 2 -- Use option
|
||
let mut ret := match userName? with
|
||
| some userName => f!"case {userName}"
|
||
| none => Format.nil
|
||
for hyp in g.hyps do
|
||
ret := addLine ret
|
||
let names := hyp.names
|
||
|>.toList
|
||
|>.filter (· != toString Name.anonymous)
|
||
|> " ".intercalate
|
||
match names with
|
||
| "" =>
|
||
ret := ret ++ Format.group f!":{Format.nest indent (Format.line ++ hyp.type.stripTags)}"
|
||
| _ =>
|
||
match hyp.val? with
|
||
| some val =>
|
||
ret := ret ++ Format.group f!"{names} : {hyp.type.stripTags} :={Format.nest indent (Format.line ++ val.stripTags)}"
|
||
| none =>
|
||
ret := ret ++ Format.group f!"{names} :{Format.nest indent (Format.line ++ hyp.type.stripTags)}"
|
||
ret := addLine ret
|
||
ret ++ f!"{goalPrefix}{Format.nest indent g.type.stripTags}"
|
||
where
|
||
addLine (fmt : Format) : Format :=
|
||
if fmt.isNil then fmt else fmt ++ Format.line
|
||
|
||
def InteractiveGoal.pretty (g : InteractiveGoal) : Format :=
|
||
g.toInteractiveGoalCore.pretty g.userName? g.goalPrefix
|
||
|
||
def InteractiveTermGoal.pretty (g : InteractiveTermGoal) : Format :=
|
||
g.toInteractiveGoalCore.pretty none "⊢ "
|
||
|
||
structure InteractiveGoals where
|
||
goals : Array InteractiveGoal
|
||
deriving RpcEncodable
|
||
|
||
def InteractiveGoals.append (l r : InteractiveGoals) : InteractiveGoals where
|
||
goals := l.goals ++ r.goals
|
||
|
||
instance : Append InteractiveGoals := ⟨InteractiveGoals.append⟩
|
||
instance : EmptyCollection InteractiveGoals := ⟨{goals := #[]}⟩
|
||
|
||
open Meta in
|
||
/-- Extend an array of hypothesis bundles with another bundle. -/
|
||
def addInteractiveHypothesisBundle (hyps : Array InteractiveHypothesisBundle)
|
||
(ids : Array (String × FVarId)) (type : Expr) (value? : Option Expr := none) :
|
||
MetaM (Array InteractiveHypothesisBundle) := do
|
||
if ids.size == 0 then
|
||
throwError "Can only add a nonzero number of ids as an InteractiveHypothesisBundle."
|
||
let fvarIds := ids.map Prod.snd
|
||
let names := ids.map Prod.fst
|
||
return hyps.push {
|
||
names
|
||
fvarIds
|
||
type := (← ppExprTagged type)
|
||
val? := (← value?.mapM ppExprTagged)
|
||
isInstance? := if (← isClass? type).isSome then true else none
|
||
isType? := if (← instantiateMVars type).isSort then true else none
|
||
}
|
||
|
||
open Meta in
|
||
variable [MonadControlT MetaM n] [Monad n] [MonadError n] [MonadOptions n] [MonadMCtx n] in
|
||
def withGoalCtx (goal : MVarId) (action : LocalContext → MetavarDecl → n α) : n α := do
|
||
let mctx ← getMCtx
|
||
let some mvarDecl := mctx.findDecl? goal
|
||
| throwError "unknown goal {goal.name}"
|
||
let lctx := mvarDecl.lctx |>.sanitizeNames.run' {options := (← getOptions)}
|
||
withLCtx lctx mvarDecl.localInstances (action lctx mvarDecl)
|
||
|
||
open Meta in
|
||
/-- A variant of `Meta.ppGoal` which preserves subexpression information for interactivity. -/
|
||
def goalToInteractive (mvarId : MVarId) : MetaM InteractiveGoal := do
|
||
let ppAuxDecls := pp.auxDecls.get (← getOptions)
|
||
let ppImplDetailHyps := pp.implementationDetailHyps.get (← getOptions)
|
||
let showLetValues := pp.showLetValues.get (← getOptions)
|
||
withGoalCtx mvarId fun lctx mvarDecl => do
|
||
let pushPending (ids : Array (String × FVarId)) (type? : Option Expr) (hyps : Array InteractiveHypothesisBundle)
|
||
: MetaM (Array InteractiveHypothesisBundle) :=
|
||
if ids.isEmpty then
|
||
pure hyps
|
||
else
|
||
match type? with
|
||
| none => pure hyps
|
||
| some type => addInteractiveHypothesisBundle hyps ids type
|
||
let mut varNames : Array (String × FVarId) := #[]
|
||
let mut prevType? : Option Expr := none
|
||
let mut hyps : Array InteractiveHypothesisBundle := #[]
|
||
for localDecl in lctx do
|
||
if !ppAuxDecls && localDecl.isAuxDecl || !ppImplDetailHyps && localDecl.isImplementationDetail then
|
||
continue
|
||
else
|
||
match localDecl with
|
||
| LocalDecl.cdecl _index fvarId varName type _ _ =>
|
||
-- We rely on the fact that `withGoalCtx` runs `LocalContext.sanitizeNames`,
|
||
-- so the `userName`s of local hypotheses are already pretty-printed
|
||
-- and it suffices to simply `toString` them.
|
||
let varName := toString varName
|
||
let type ← instantiateMVars type
|
||
if prevType? == none || prevType? == some type then
|
||
varNames := varNames.push (varName, fvarId)
|
||
else
|
||
hyps ← pushPending varNames prevType? hyps
|
||
varNames := #[(varName, fvarId)]
|
||
prevType? := some type
|
||
| LocalDecl.ldecl _index fvarId varName type val _ _ => do
|
||
let varName := toString varName
|
||
hyps ← pushPending varNames prevType? hyps
|
||
let type ← instantiateMVars type
|
||
let val? ← if showLetValues then pure (some (← instantiateMVars val)) else pure none
|
||
hyps ← addInteractiveHypothesisBundle hyps #[(varName, fvarId)] type val?
|
||
varNames := #[]
|
||
prevType? := none
|
||
hyps ← pushPending varNames prevType? hyps
|
||
let goalTp ← instantiateMVars mvarDecl.type
|
||
let goalFmt ← ppExprTagged goalTp
|
||
let userName? := match mvarDecl.userName with
|
||
| Name.anonymous => none
|
||
| name => some <| toString name.eraseMacroScopes
|
||
return {
|
||
hyps
|
||
type := goalFmt
|
||
ctx := ⟨{← Elab.CommandContextInfo.save with }⟩
|
||
userName?
|
||
goalPrefix := getGoalPrefix mvarDecl
|
||
mvarId
|
||
}
|
||
|
||
end Lean.Widget
|