mikeljl's picture
use pypantograph for checking submitted statements
0115dcf
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 "<failed to pretty print>"
/-- 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 }