import Pantograph.Frontend.InfoTree import Pantograph.Frontend.MetaTranslate import Pantograph.Frontend.Refactor import Pantograph.Goal import Pantograph.Protocol open Lean namespace Pantograph.Frontend -- Info tree filtering functions /- Adapted from lean-training-data -/ structure TacticInvocation where info : Elab.TacticInfo ctx : Elab.ContextInfo children : PersistentArray Elab.InfoTree namespace TacticInvocation /-- Return the range of the tactic, as a pair of file positions. -/ @[export pantograph_frontend_tactic_invocation_range] protected def range (t : TacticInvocation) : Position × Position := t.ctx.fileMap.stxRange t.info.stx /-- Pretty print a tactic. -/ protected def pp (t : TacticInvocation) : IO Format := t.ctx.runMetaM {} try Lean.PrettyPrinter.ppTactic ⟨t.info.stx⟩ catch _ => pure "" /-- Run a tactic on the goals stored in a `TacticInvocation`. -/ protected def runMetaMGoalsBefore (t : TacticInvocation) (x : List MVarId → MetaM α) : IO α := do t.ctx.runMetaM {} <| Meta.withMCtx t.info.mctxBefore <| x t.info.goalsBefore /-- Run a tactic on the after goals stored in a `TacticInvocation`. -/ protected def runMetaMGoalsAfter (t : TacticInvocation) (x : List MVarId → MetaM α) : IO α := do t.ctx.runMetaM {} <| Meta.withMCtx t.info.mctxAfter <| x t.info.goalsAfter /-- Run a tactic on the main goal stored in a `TacticInvocation`. -/ protected def runMetaM (t : TacticInvocation) (x : MVarId → MetaM α) : IO α := do match t.info.goalsBefore.head? with | none => throw <| IO.userError s!"No goals at {← t.pp}" | some g => t.runMetaMGoalsBefore fun _ => do g.withContext <| x g protected def goalState (t : TacticInvocation) : IO (List Format) := do t.runMetaMGoalsBefore (fun gs => gs.mapM fun g => do Meta.ppGoal g) protected def goalStateAfter (t : TacticInvocation) : IO (List Format) := do t.runMetaMGoalsAfter (fun gs => gs.mapM fun g => do Meta.ppGoal g) protected def usedConstants (t: TacticInvocation) : NameSet := let info := t.info info.goalsBefore |>.filterMap info.mctxAfter.getExprAssignmentCore? |>.map Expr.getUsedConstantsAsSet |>.foldl .append .empty end TacticInvocation /-- Return all `TacticInfo` nodes in an `InfoTree` corresponding to tactics, each equipped with its relevant `ContextInfo`, and any children info trees. -/ private def collectTacticNodes (t : Elab.InfoTree) : List TacticInvocation := let infos := t.findAllInfo none false fun i => match i with | .ofTacticInfo _ => true | _ => false infos.filterMap fun p => match p with | (.ofTacticInfo i, some ctx, children) => .some ⟨i, ctx, children⟩ | _ => none def collectTactics (t : Elab.InfoTree) : List TacticInvocation := collectTacticNodes t |>.filter fun i => i.info.isSubstantive @[export pantograph_frontend_collect_tactics_from_compilation_step_m] def collectTacticsFromCompilationStep (step : CompilationStep) : IO (List Protocol.InvokedTactic) := do let tacticInfoTrees := step.trees.flatMap λ tree => tree.filter λ | info@(.ofTacticInfo _) => info.isOriginal | _ => false let tactics := tacticInfoTrees.flatMap collectTactics tactics.mapM λ invocation => do let goalBefore := (Format.joinSep (← invocation.goalState) "\n").pretty let goalAfter := (Format.joinSep (← invocation.goalStateAfter) "\n").pretty let tactic ← invocation.ctx.runMetaM {} <| Meta.withMCtx invocation.info.mctxBefore do return (← invocation.ctx.ppSyntax {} invocation.info.stx).pretty -- FIXME: Why does this not work? There are problems with `term.pseudo.antiquot` --PrettyPrinter.ppTactic ⟨invocation.info.stx⟩ --return t.pretty let usedConstants := invocation.usedConstants.toArray return { goalBefore, goalAfter, tactic, usedConstants, } structure InfoWithContext where info: Elab.Info context?: Option Elab.ContextInfo := .none structure GoalCollectionOptions where collectTypeErrors : Bool := false private def collectSorrysInTree (t : Elab.InfoTree) (options : GoalCollectionOptions := {}) : IO (List InfoWithContext) := do let infos ← t.findAllInfoM none fun i ctx? => match i with | .ofTermInfo { expectedType?, expr, stx, lctx, isBinder := false, .. } => do let .some ctx := ctx? | return (false, true) if expr.isSorry ∧ stx.isOfKind `Lean.Parser.Term.sorry then if expectedType?.isNone then throw $ .userError "Sorry of indeterminant type is not allowed" return (true, false) unless options.collectTypeErrors do return (false, true) let .some expectedType := expectedType? | return (false, true) let typeMatch ← ctx.runMetaM lctx do let type ← Meta.inferType expr Meta.isExprDefEqGuarded type expectedType return match typeMatch, expr.hasSorry with | false, true => (true, false) -- Types mismatch but has sorry -> collect, halt | false, false => (true, false) -- Types mistmatch but no sorry -> collect, halt | true, true => (false, true) -- Types match but has sorry -> continue | true, false => (false, false) -- Types match but no sorries -> halt | .ofTacticInfo { stx, goalsBefore, .. } => -- The `sorry` term is distinct from the `sorry` tactic let isSorry := stx.isOfKind `Lean.Parser.Tactic.tacticSorry return (isSorry ∧ !goalsBefore.isEmpty, ¬ isSorry) | _ => return (false, true) return infos.map fun (info, context?, _) => { info, context? } -- NOTE: Plural deliberately not spelled "sorries" @[export pantograph_frontend_collect_sorrys_m] def collectSorrys (step: CompilationStep) (options : GoalCollectionOptions := {}) : IO (List InfoWithContext) := do return (← step.trees.mapM $ λ tree => collectSorrysInTree tree options).flatten structure AnnotatedGoalState where state : GoalState srcBoundaries : List (String.Pos × String.Pos) /-- Since we cannot directly merge `MetavarContext`s, we have to get creative. This function duplicates frozen mvars in term and tactic info nodes, and add them to the current `MetavarContext`. DEPRECATED: Behaviour is unstable when there are multiple `sorry`s. Consider using the draft tactic instead. -/ @[export pantograph_frontend_sorrys_to_goal_state_m] def sorrysToGoalState (sorrys : List InfoWithContext) : MetaM AnnotatedGoalState := do assert! !sorrys.isEmpty let goalsM := sorrys.mapM λ i => do match i.info with | .ofTermInfo termInfo => do let mvarId ← MetaTranslate.translateMVarFromTermInfo termInfo i.context? if (← mvarId.getType).hasSorry then throwError s!"Coupling is not allowed in drafting" return [(mvarId, stxByteRange termInfo.stx)] | .ofTacticInfo tacticInfo => do let mvarIds ← MetaTranslate.translateMVarFromTacticInfoBefore tacticInfo i.context? for mvarId in mvarIds do if (← mvarId.getType).hasSorry then throwError s!"Coupling is not allowed in drafting" let range := stxByteRange tacticInfo.stx return mvarIds.map (·, range) | _ => panic! "Invalid info" let annotatedGoals := List.flatten (← goalsM.run {} |>.run' {}) let goals := annotatedGoals.map Prod.fst let srcBoundaries := annotatedGoals.map Prod.snd let root := match goals with | [] => panic! "No MVars generated" | [g] => g | _ => { name := .anonymous } let state ← GoalState.createFromMVars goals root return { state, srcBoundaries } structure DistilConfig where binderName? : Option Name := .none ignoreValues : Bool := true structure DistilledSearchTarget where goalState : GoalState def distilGoalStateFrom (head : Refactor.Command) (tail : List Refactor.Command) (config : DistilConfig) : RefactorM DistilledSearchTarget := do if head.constants.isEmpty then throw $ .userError "No constants in head declaration" let headName := head.constants.toList.head! let binderName := match config.binderName?, headName with | .some n, _ => n | _, .str _ binderName => Name.mkSimple binderName | _, _ => `x Refactor.distilSearchTarget head tail λ (witness, witnessValue) companions => do if companions.isEmpty then Meta.check witness -- Without companions, we can directly construct a goal state let goalState ← GoalState.create witness let goalState ← if !config.ignoreValues then goalState.step .unfocus do let goal ← Elab.Tactic.getMainGoal let (value, witnessGoals) ← Tactic.sorryToHole witnessValue |>.run [] Meta.check value goal.assign value Elab.Tactic.setGoals witnessGoals else pure goalState return { goalState } else let companion ← Meta.withLocalDeclD binderName witness λ binder => do let companion ← Refactor.mkProdElem ``And <| companions.map (·.fst.instantiate1 binder) Meta.mkLambdaFVars #[binder] companion let target ← Meta.mkAppOptM ``Subtype #[witness, companion] let goalState ← GoalState.create target let goalState ← if !config.ignoreValues && (!witnessValue.isSorry || companions.any (!·.snd.isSorry)) then goalState.step .unfocus do let goal ← Elab.Tactic.getMainGoal -- Construct the solution expression let (witnessValue', witnessGoals) ← Tactic.sorryToHole witnessValue |>.run [] assert! !witnessValue'.hasSorry let companionValue ← Meta.withLocalDeclD binderName witness λ binder => do let v ← Refactor.mkProdElem ``And.intro <| companions.map λ (_, value) => value.instantiate1 binder Meta.mkLambdaFVars #[binder] v let (companionValue', companionGoals) ← Tactic.sorryToHole (companionValue.beta #[witnessValue']) |>.run [] if h : witnessGoals.length = 1 then let witnessGoal := witnessGoals[0] witnessGoal.setTag binderName let value ← Meta.mkAppOptM ``Subtype.mk #[ witness, companion, witnessValue', companionValue', ] Meta.check value goal.assign value Elab.Tactic.setGoals (witnessGoals ++ companionGoals.reverse) else pure goalState return { goalState } open Refactor in def distilSearchTargets (env : Environment) (source : String) (config : DistilConfig := {}) (fileName : String := defaultFileName) : IO (List DistilledSearchTarget) := do let (fContext, fState) ← createContextStateFromFile source fileName env {} let commands ← Refactor.preprocess.run {} |>.run fContext |>.run' fState let errors := commands.filter (·.hasError) if let .some error := errors.head? then let message ← error.messages.mapM (·.toString) throw $ IO.userError $ "\n".intercalate message let m : RefactorM (List DistilledSearchTarget) := do let mut targets := [] while !(← get).commands.isEmpty do let { commands, .. } ← get let decl :: commands := commands | Refactor.fail "No commands left" modify ({ · with commands }) -- Prevents infinite loop let isSearchTarget ← decl.runCoreM do if decl.constants.isEmpty then return false let name := decl.constants.toList.head! let info := (← getEnv).find? name |>.get! let .some value := info.value? | return false return value.hasSorry if !isSearchTarget then pushNewCommand' (⟨decl.stx⟩ : Syntax.Command) continue let depstr ← extractDependencyStructure decl commands modify ({ · with commands := depstr.tail }) for command in depstr.intercalating do pushNewCommand' (⟨command.stx⟩ : Syntax.Command) let searchTarget ← distilGoalStateFrom decl depstr.component config targets := targets ++ [searchTarget] pure targets let outContext := { fContext.inputCtx with inputString := "", fileMap := "".toFileMap, endPos := "".endPos, endPos_valid := by simp, } let parserState := {} let outState := { commandState := Elab.Command.mkState env {} {}, parserState, cmdPos := parserState.pos, } m.run { inContext := fContext.inputCtx } |>.run' { outContext, outState, commands }