mikeljl's picture
use pypantograph for checking submitted statements
0115dcf
/- A scrolling refactor algorithm: The algorithm ingests Lean compilation units
on one end and outputs compilation units on the other. -/
import Pantograph.Frontend.Basic
import Pantograph.Frontend.InfoTree
import Pantograph.Delate
open Lean
namespace Pantograph.Frontend
namespace Refactor
/-- A command in the input file, frozen in context -/
structure Command where
dependencies : NameSet := .empty
stx : Syntax
trees : List Elab.InfoTree
hasError : Bool := false
constants : NameSet := .empty
state : Elab.Command.State
messages : List Message
inductive CommandCategory where
-- Definition of data
| data
-- Definition fo theorems
| declaration
-- section, variable, universe
| flow
-- Things which can be discarded.
| auxiliary
-- No refactor units may cross an `.unknown` boundary.
| unknown
protected def Command.category (command : Command) : CommandCategory :=
match command.stx.getKind with
| `Lean.Parser.Command.declaration =>
match command.stx.getArg 2 |>.getKind with
| `Lean.Parser.Command.structure
| `Lean.Parser.Command.inductive
=> .data
| `Lean.Parser.Command.theorem
| `Lean.Parser.Command.definition
=> .data
| _ => .unknown
| `Lean.Parser.Command.section
| `Lean.Parser.Command.namespace
| `Lean.Parser.Command.variable
| `Lean.Parser.Command.end
=> .flow
| `Lean.Parser.Command.set_option
=> .auxiliary
| _ => .unknown
protected def Command.comments (command : Command) : Syntax :=
let modifiers := command.stx.getArg 0
let comments := modifiers.getArg 0
comments[0]
structure Config where
coreOptions : Options := {}
deriving Inhabited
structure Context where
inContext : Parser.InputContext
config : Config := {}
structure State where
outContext : Parser.InputContext
outState : Elab.Frontend.State
-- Collected top-level units, scrolling
commands : List Command := []
/-- Two monads rolled into one -/
abbrev RefactorM := ReaderT Context $ StateRefT State IO
def readConfig : RefactorM Config := do
return (← read).config
def readCoreOptions : RefactorM Options := do
return (← readConfig).coreOptions
def fail { α } (s : String) : IO α :=
throw <| .userError s
def mergeFileMap (fm1 fm2 : FileMap) : FileMap :=
let bias := fm1.source.endPos.byteIdx + 1
let mappedPos := fm2.positions.map λ pos => { byteIdx := pos.byteIdx + bias }
{
source := s!"{fm1.source}\n{fm2.source}",
positions := fm1.positions.take (fm1.positions.size - 1) ++ mappedPos
}
/-- Add one command to the refactored file -/
def pushNewCommand (f : Format) : RefactorM Unit := do
modify λ state@{ outContext := outContext@{ fileMap, .. }, .. } =>
let payload := f.pretty
let merged := mergeFileMap fileMap payload.toFileMap
{
state with outContext := {
outContext with
inputString := merged.source,
fileMap := merged,
endPos := merged.source.endPos,
endPos_valid := by simp,
}
}
-- After modification, run the parser ahead by one position
let { outContext := inputCtx, outState, .. } ← get
let (_endFlag, outState@ { commandState := { messages, .. }, .. }) ←
Elab.Frontend.processCommand.run { inputCtx } |>.run outState
-- Ensure no error has occurred
if messages.hasErrors then
let messages ← messages.toList.mapM (·.toString)
throw (.userError s!"Error messages: {messages}")
modify ({ · with outState })
/-- Run `FrontendM` at the tail of the out file -/
def liftFrontend { α } (x : FrontendM α) : RefactorM α := do
let { outContext := inputCtx, outState, .. } ← get
x.run {} |>.run { inputCtx } |>.run' outState
/-- Run `CoreM` at the tail of the out file -/
def runCoreM { α } (x : CoreM α) : RefactorM α := do
liftFrontend $ runCommandElabM $ Elab.Command.liftCoreM x
def pushNewCommand' (command : Syntax.Command) : RefactorM Unit := do
let f ← runCoreM do
PrettyPrinter.ppCommand command
pushNewCommand f
/-- runs a "frozen" `CommandElabM` that can't modify anything. -/
@[inline] protected
def Command.runCommandElabM (command : Command) (x : Elab.Command.CommandElabM α) : RefactorM α := do
let inputCtx := (← read).inContext
let cmdCtx : Elab.Command.Context := {
fileName := inputCtx.fileName
fileMap := inputCtx.fileMap
snap? := none,
cancelTk? := .none,
}
match (← liftM <| EIO.toIO' <| (x cmdCtx).run command.state) with
| Except.error e => throw <| IO.Error.userError s!"unexpected internal error: {← e.toMessageData.toString}"
| Except.ok (a, _sNew) => return a
@[inline] protected
def Command.runCoreM { α } (command : Command) (c : CoreM α) : RefactorM α :=
command.runCommandElabM $ Elab.Command.liftCoreM c
def constantDependencies (env : Environment) (name : Name) : NameSet :=
let const := env.find? name |>.get!
let s := const.type.getUsedConstantsAsSet
let s := match const.value? with
| .some v => s.append v.getUsedConstantsAsSet
| .none => s
s
def hasSorry (step : CompilationStep) : Bool :=
step.trees.any λ tree =>
let nodes := tree.filter λ
| .ofTermInfo { expr, .. } => expr.isSorry
| .ofTacticInfo { stx, .. } => stx.isOfKind `Lean.Parser.Tactic.tacticSorry
| _ => false
!nodes.isEmpty
/-- Scroll to the end of the file, reading all compilation units in the process -/
def preprocess : FrontendM (List Command) := mapCompilationSteps λ step => do
let constants ← step.newConstants
let dependencies := constants.foldl (init := NameSet.empty) λ acc c =>
acc.append $ constantDependencies step.after c
let commandState := (← getThe Elab.Frontend.State).commandState
let unit := if step.msgs.any (·.severity == .error) then
{
hasError := true,
stx := step.stx,
trees := step.trees,
state := commandState,
messages := step.msgs,
}
else
{
stx := step.stx,
trees := step.trees,
dependencies,
constants,
state := commandState,
messages := step.msgs,
}
return unit
/-- Creates `combine (combine a[0] a[1]) a[2] ...` -/
def mkProdElem (combine : Name := ``And.intro) : List Expr → MetaM Expr
| .nil => return .const `Unit []
| [a] => return a
| x :: xs => do
let r ← mkProdElem combine xs
Meta.mkAppM combine #[x, r]
private def mkDocComment (s : String) :=
mkNode ``Parser.Command.docComment #[mkAtom "/--", mkAtom s!"{s} -/"]
protected def _root_.Array.last! { α } [Inhabited α] (a : Array α) : α :=
match h : a.size with
| 0 => default
| n+1 => a[n]
def distilSearchTarget { α } (head : Command) (tail : List Command) (f : (Expr × Expr) → List (Expr × Expr) → Elab.Term.TermElabM α)
: RefactorM α := do
let (headName, witness, witnessValue) ← head.runCommandElabM do
Elab.Command.liftCoreM do
let env := head.state.env
let name := head.constants.toList.head!
let info := env.find? name |>.get!
let .some value := info.value?
| throwError s!"Constant has no value: {name}"
return (name, ← normalize info.type, ← normalize value)
let companions ← tail.mapM λ command => command.runCommandElabM do
Elab.Command.liftTermElabM do
let env := command.state.env
let name := command.constants.toList.head!
let info := env.find? name |>.get!
let type ← normalize info.type
let c ← mkConstWithLevelParams headName
let type ← Meta.kabstract type c
let .some value := info.value?
| throwError s!"Constant has no value: {name}"
-- Normalization strips away matchers.
let value ← normalize value
let value ← Meta.kabstract value c
return (type, value)
liftFrontend $ runCommandElabM $ Elab.Command.liftTermElabM do
f (witness, witnessValue) companions
where
normalize (e : Expr) : CoreM Expr := do
unfoldAuxLemmas $ ← unfoldMatchers e
/-- Fold `sorry`s into one definition -/
def foldTheoremsFlat (head : Command) (tail : List Command) : RefactorM Syntax.Command := do
-- Concatenate all doc comments
let allDocs := "\n".intercalate $ (head :: tail).filterMap λ command =>
let `(docComment|$comment) := command.comments
let s := comment.getDocString
if s.isEmpty then .none else s
let headName := head.constants.toList.head!
let binderName := match headName with
| .str _ binderName => Name.mkSimple binderName
| _ => `x
let coreOptions ← readCoreOptions
distilSearchTarget head tail λ (witness, _) companions => do
-- Construct the companion
let companion ← Meta.withLocalDeclD binderName witness λ binder => do
let companion ← mkProdElem ``And <| companions.map (·.fst.instantiate1 binder)
Meta.mkLambdaFVars #[binder] companion
let target ← Meta.mkAppOptM ``Subtype #[witness, companion]
Meta.check target
-- Delaborate this back into syntax
let target ← withOptions (λ _ => pp.analyze.set coreOptions true) do
PrettyPrinter.delab target
let theoremIdent := mkIdent $ Name.mkSimple s!"{binderName}_composite"
let comment? := if allDocs.isEmpty then .none else .some $ mkDocComment allDocs
`(command|$[$comment?:docComment]? def $theoremIdent : $target := sorry)
structure DependencyTracker where
-- Constants generated during the next batch of commands to be processed
innerConstants : NameSet := {}
isNonFlat : Bool := false
structure DependencyStructure where
-- Commands which depend on each other
component : List Command := []
-- Intercalating commands
intercalating : List Command := []
-- Remainder
tail : List Command
-- Structure
tracker : DependencyTracker
def extractDependencyStructure (head : Command) (commands : List Command)
: RefactorM DependencyStructure := do
let ((series, other), tracker) := (λ (z : StateM DependencyTracker _) => z.run {}) $
commands.zipIdx.partitionM λ (command, _) => do
let tracker ← get
if command.dependencies.any tracker.innerConstants.contains then
let innerConstants := command.constants.foldl
(init := tracker.innerConstants)
λ acc n => acc.insert n
modify ({· with innerConstants, isNonFlat := true})
return true
if command.dependencies.any head.constants.contains then
let innerConstants := command.constants.foldl
(init := tracker.innerConstants)
λ acc n => acc.insert n
modify ({· with innerConstants })
return true
else
return false
if series.isEmpty then
return {
tail := commands,
tracker,
}
-- Find all intercalating declarations
let maxIdx := series.map Prod.snd |>.max?.get!
let (intercalating, tail) := other.partition λ (_, idx) => idx < maxIdx
return {
component := series.map Prod.fst,
intercalating := intercalating.map Prod.fst,
tail := tail.map Prod.fst,
tracker
}
/-- Scroll one unit down from the top -/
def collectNextCommand : RefactorM Unit := 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)
return
let depstr ← extractDependencyStructure decl commands
if depstr.component.isEmpty then
pushNewCommand' (⟨decl.stx⟩ : Syntax.Command)
return
if depstr.tracker.isNonFlat then
Refactor.fail "Cannot refactor non-flat dependency structure"
modify ({ · with commands := depstr.tail })
-- Push all intercalating commands
for command in depstr.intercalating do
pushNewCommand' (⟨command.stx⟩ : Syntax.Command)
let f ← foldTheoremsFlat decl depstr.component
pushNewCommand' f
end Refactor
open Refactor in
def runRefactor (env : Environment) (source : String)
(config : Refactor.Config := {}) (fileName := defaultFileName) : IO String := do
let (fContext, fState) ← createContextStateFromFile source fileName env {}
let commands ← 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 Unit := do
while !(← get).commands.isEmpty do
collectNextCommand
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,
}
let (_, state) ← m.run { config, inContext := fContext.inputCtx }
|>.run { outContext, outState, commands }
return state.outContext.inputString
export Refactor (RefactorM)