/- 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)