Compare commits

...

No commits in common. "io/async" and "dev" have entirely different histories.

243 changed files with 6920 additions and 59231 deletions

View File

@ -1,54 +0,0 @@
name: deploy-docs
on:
push:
branches:
- main
# This job installs dependencies, builds the book, and pushes it to `gh-pages`
jobs:
deploy-book:
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install elan
run: |
set -o pipefail
curl -sSfL https://github.com/leanprover/elan/releases/download/v3.1.1/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
./elan-init -y --default-toolchain none
echo "$HOME/.elan/bin" >> "${GITHUB_PATH}"
- name: Install Lean
run: |
elan toolchain install $(<src/lean-toolchain)
- name: Install poetry
run: |
pip install poetry
poetry install --only doc
- name: Build documentations
run: |
poetry run jupyter-book build docs
# Upload the book's HTML as an artifact
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: "docs/_build/html"
# Deploy the book's HTML to GitHub Pages
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

11
.gitignore vendored
View File

@ -1,11 +1,4 @@
.*
!.gitignore
!.github
# Python
*.py[cod]
*.egg-info
# Output
/dist
/venv
*.[io]lean
/result

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "src"]
path = src
url = https://github.com/lenianiva/Pantograph.git

72
Main.lean Normal file
View File

@ -0,0 +1,72 @@
import Lean.Data.Json
import Lean.Environment
import Pantograph
import Repl
-- Main IO functions
open Pantograph.Repl
open Pantograph.Protocol
/-- Parse a command either in `{ "cmd": ..., "payload": ... }` form or `cmd { ... }` form. -/
def parseCommand (s: String): Except String Command := do
let s := s.trim
match s.get? 0 with
| .some '{' => -- Parse in Json mode
Lean.fromJson? (← Lean.Json.parse s)
| .some _ => -- Parse in line mode
let offset := s.posOf ' ' |> s.offsetOfPos
if offset = s.length then
return { cmd := s.take offset, payload := Lean.Json.null }
else
let payload ← s.drop offset |> Lean.Json.parse
return { cmd := s.take offset, payload := payload }
| .none => throw "Command is empty"
partial def loop : MainM Unit := do
let state ← get
let command ← (← IO.getStdin).getLine
if command.trim.length = 0 then return ()
match parseCommand command with
| .error error =>
let error := Lean.toJson ({ error := "command", desc := error }: InteractionError)
-- Using `Lean.Json.compress` here to prevent newline
IO.println error.compress
| .ok command =>
try
let ret ← execute command
let str := match state.options.printJsonPretty with
| true => ret.pretty
| false => ret.compress
IO.println str
catch e =>
let message ← e.toMessageData.toString
let error := Lean.toJson ({ error := "main", desc := message }: InteractionError)
IO.println error.compress
loop
unsafe def main (args: List String): IO Unit := do
-- NOTE: A more sophisticated scheme of command line argument handling is needed.
-- Separate imports and options
if args == ["--version"] then do
IO.println s!"{Pantograph.version}"
return
Pantograph.initSearch ""
let coreContext ← args.filterMap (λ s => if s.startsWith "--" then .some <| s.drop 2 else .none)
|>.toArray |> Pantograph.createCoreContext
let imports:= args.filter (λ s => ¬ (s.startsWith "--"))
let coreState ← Pantograph.createCoreState imports.toArray
let context: Context := {
imports
}
try
let coreM := loop.run context |>.run' {}
IO.println "ready."
discard <| coreM.toIO coreContext coreState
catch ex =>
let message := ex.toString
let error := Lean.toJson ({ error := "io", desc := message }: InteractionError)
IO.println error.compress

9
Pantograph.lean Normal file
View File

@ -0,0 +1,9 @@
import Pantograph.Delate
import Pantograph.Elab
import Pantograph.Environment
import Pantograph.Frontend
import Pantograph.Goal
import Pantograph.Library
import Pantograph.Protocol
import Pantograph.Serial
import Pantograph.Version

561
Pantograph/Delate.lean Normal file
View File

@ -0,0 +1,561 @@
/-
This file handles "Delation": The conversion of Kernel view into Search view.
-/
import Lean
import Std.Data.HashMap
import Pantograph.Goal
import Pantograph.Protocol
open Lean
-- Symbol processing functions --
namespace Pantograph
structure ProjectionApplication where
projector: Name
numParams: Nat
inner: Expr
@[export pantograph_expr_proj_to_app]
def exprProjToApp (env: Environment) (e: Expr): ProjectionApplication :=
let (typeName, idx, inner) := match e with
| .proj typeName idx inner => (typeName, idx, inner)
| _ => panic! "Argument must be proj"
let ctor := getStructureCtor env typeName
let fieldName := getStructureFields env typeName |>.get! idx
let projector := getProjFnForField? env typeName fieldName |>.get!
{
projector,
numParams := ctor.numParams,
inner,
}
def _root_.Lean.Name.isAuxLemma (n : Lean.Name) : Bool := n matches .num (.str _ "_auxLemma") _
/-- Unfold all lemmas created by `Lean.Meta.mkAuxLemma`. These end in `_auxLemma.nn` where `nn` is a number. -/
@[export pantograph_unfold_aux_lemmas]
def unfoldAuxLemmas (e : Expr) : CoreM Expr := do
Lean.Meta.deltaExpand e Lean.Name.isAuxLemma
/--
Force the instantiation of delayed metavariables even if they cannot be fully
instantiated. This is used during resumption to provide diagnostic data about
the current goal.
Since Lean 4 does not have an `Expr` constructor corresponding to delayed
metavariables, any delayed metavariables must be recursively handled by this
function to ensure that nested delayed metavariables can be properly processed.
The caveat is this recursive call will lead to infinite recursion if a loop
between metavariable assignment exists.
This function ensures any metavariable in the result is either
1. Delayed assigned with its pending mvar not assigned in any form
2. Not assigned (delay or not)
-/
partial def instantiateDelayedMVars (eOrig: Expr) : MetaM Expr := do
--let padding := String.join $ List.replicate level "│ "
--IO.println s!"{padding}Starting {toString eOrig}"
let mut result ← Meta.transform (← instantiateMVars eOrig)
(pre := fun e => e.withApp fun f args => do
let .mvar mvarId := f | return .continue
--IO.println s!"{padding}├V {e}"
let mvarDecl ← mvarId.getDecl
-- This is critical to maintaining the interdependency of metavariables.
-- Without setting `.syntheticOpaque`, Lean's metavariable elimination
-- system will not make the necessary delayed assigned mvars in case of
-- nested mvars.
mvarId.setKind .syntheticOpaque
mvarId.withContext do
let lctx ← MonadLCtx.getLCtx
if mvarDecl.lctx.any (λ decl => !lctx.contains decl.fvarId) then
let violations := mvarDecl.lctx.decls.foldl (λ acc decl? => match decl? with
| .some decl => if lctx.contains decl.fvarId then acc else acc ++ [decl.fvarId.name]
| .none => acc) []
panic! s!"In the context of {mvarId.name}, there are local context variable violations: {violations}"
if let .some assign ← getExprMVarAssignment? mvarId then
--IO.println s!"{padding}├A ?{mvarId.name}"
assert! !(← mvarId.isDelayedAssigned)
return .visit (mkAppN assign args)
else if let some { fvars, mvarIdPending } ← getDelayedMVarAssignment? mvarId then
--let substTableStr := String.intercalate ", " $ Array.zipWith fvars args (λ fvar assign => s!"{fvar.fvarId!.name} := {assign}") |>.toList
--IO.println s!"{padding}├MD ?{mvarId.name} := ?{mvarIdPending.name} [{substTableStr}]"
if args.size < fvars.size then
throwError "Not enough arguments to instantiate a delay assigned mvar. This is due to bad implementations of a tactic: {args.size} < {fvars.size}. Expr: {toString e}; Origin: {toString eOrig}"
--if !args.isEmpty then
--IO.println s!"{padding}├── Arguments Begin"
let args ← args.mapM self
--if !args.isEmpty then
--IO.println s!"{padding}├── Arguments End"
if !(← mvarIdPending.isAssignedOrDelayedAssigned) then
--IO.println s!"{padding}├T1"
let result := mkAppN f args
return .done result
let pending ← mvarIdPending.withContext do
let inner ← instantiateDelayedMVars (.mvar mvarIdPending) --(level := level + 1)
--IO.println s!"{padding}├Pre: {inner}"
pure <| (← inner.abstractM fvars).instantiateRev args
-- Tail arguments
let result := mkAppRange pending fvars.size args.size args
--IO.println s!"{padding}├MD {result}"
return .done result
else
assert! !(← mvarId.isAssigned)
assert! !(← mvarId.isDelayedAssigned)
--if !args.isEmpty then
-- IO.println s!"{padding}├── Arguments Begin"
let args ← args.mapM self
--if !args.isEmpty then
-- IO.println s!"{padding}├── Arguments End"
--IO.println s!"{padding}├M ?{mvarId.name}"
return .done (mkAppN f args))
--IO.println s!"{padding}└Result {result}"
return result
where
self e := instantiateDelayedMVars e --(level := level + 1)
/--
Convert an expression to an equiavlent form with
1. No nested delayed assigned mvars
2. No aux lemmas
3. No assigned mvars
-/
@[export pantograph_instantiate_all_m]
def instantiateAll (e: Expr): MetaM Expr := do
let e ← instantiateDelayedMVars e
let e ← unfoldAuxLemmas e
return e
structure DelayedMVarInvocation where
mvarIdPending: MVarId
args: Array (FVarId × (Option Expr))
-- Extra arguments applied to the result of this substitution
tail: Array Expr
-- The pending mvar of any delayed assigned mvar must not be assigned in any way.
@[export pantograph_to_delayed_mvar_invocation_m]
def toDelayedMVarInvocation (e: Expr): MetaM (Option DelayedMVarInvocation) := do
let .mvar mvarId := e.getAppFn | return .none
let .some decl ← getDelayedMVarAssignment? mvarId | return .none
let mvarIdPending := decl.mvarIdPending
let mvarDecl ← mvarIdPending.getDecl
-- Print the function application e. See Lean's `withOverApp`
let args := e.getAppArgs
assert! args.size ≥ decl.fvars.size
assert! !(← mvarIdPending.isAssigned)
assert! !(← mvarIdPending.isDelayedAssigned)
let fvarArgMap: Std.HashMap FVarId Expr := Std.HashMap.ofList $ (decl.fvars.map (·.fvarId!) |>.zip args).toList
let subst ← mvarDecl.lctx.foldlM (init := []) λ acc localDecl => do
let fvarId := localDecl.fvarId
let a := fvarArgMap[fvarId]?
return acc ++ [(fvarId, a)]
assert! decl.fvars.all (λ fvar => mvarDecl.lctx.findFVar? fvar |>.isSome)
return .some {
mvarIdPending,
args := subst.toArray,
tail := args.toList.drop decl.fvars.size |>.toArray,
}
-- Condensed representation
namespace Condensed
-- Mirrors Lean's LocalDecl
structure LocalDecl where
-- Default value is for testing
fvarId: FVarId := { name := .anonymous }
userName: Name
-- Normalized expression
type : Expr
value? : Option Expr := .none
structure Goal where
mvarId: MVarId := { name := .anonymous }
userName: Name := .anonymous
context: Array LocalDecl
target: Expr
@[export pantograph_goal_is_lhs]
def isLHS (g: Goal) : Bool := isLHSGoal? g.target |>.isSome
end Condensed
-- Get the list of visible (by default) free variables from a goal
@[export pantograph_visible_fvars_of_mvar]
protected def visibleFVarsOfMVar (mctx: MetavarContext) (mvarId: MVarId): Option (Array FVarId) := do
let mvarDecl ← mctx.findDecl? mvarId
let lctx := mvarDecl.lctx
return lctx.decls.foldl (init := #[]) fun r decl? => match decl? with
| some decl => if decl.isAuxDecl decl.isImplementationDetail then r else r.push decl.fvarId
| none => r
@[export pantograph_to_condensed_goal_m]
def toCondensedGoal (mvarId: MVarId): MetaM Condensed.Goal := do
let ppAuxDecls := Meta.pp.auxDecls.get (← getOptions)
let ppImplDetailHyps := Meta.pp.implementationDetailHyps.get (← getOptions)
let mvarDecl ← mvarId.getDecl
let lctx := mvarDecl.lctx
let lctx := lctx.sanitizeNames.run' { options := (← getOptions) }
Meta.withLCtx lctx mvarDecl.localInstances do
let ppVar (localDecl : LocalDecl) : MetaM Condensed.LocalDecl := do
match localDecl with
| .cdecl _ fvarId userName type _ _ =>
let type ← instantiate type
return { fvarId, userName, type }
| .ldecl _ fvarId userName type value _ _ => do
let userName := userName.simpMacroScopes
let type ← instantiate type
let value ← instantiate value
return { fvarId, userName, type, value? := .some value }
let vars ← lctx.foldlM (init := []) fun acc (localDecl : LocalDecl) => do
let skip := !ppAuxDecls && localDecl.isAuxDecl ||
!ppImplDetailHyps && localDecl.isImplementationDetail
if skip then
return acc
else
let var ← ppVar localDecl
return var::acc
return {
mvarId,
userName := mvarDecl.userName,
context := vars.reverse.toArray,
target := ← instantiate mvarDecl.type
}
where
instantiate := instantiateAll
@[export pantograph_goal_state_to_condensed_m]
protected def GoalState.toCondensed (state: GoalState):
CoreM (Array Condensed.Goal):= do
let metaM := do
let goals := state.goals.toArray
goals.mapM fun goal => do
match state.mctx.findDecl? goal with
| .some _ =>
let serializedGoal ← toCondensedGoal goal
pure serializedGoal
| .none => throwError s!"Metavariable does not exist in context {goal.name}"
metaM.run' (s := state.savedState.term.meta.meta)
def typeExprToBound (expr: Expr): MetaM Protocol.BoundExpression := do
Meta.forallTelescope expr fun arr body => do
let binders ← arr.mapM fun fvar => do
return (toString (← fvar.fvarId!.getUserName), toString (← Meta.ppExpr (← fvar.fvarId!.getType)))
return { binders, target := toString (← Meta.ppExpr body) }
def serializeName (name: Name) (sanitize: Bool := true): String :=
let internal := name.isInaccessibleUserName || name.hasMacroScopes
if sanitize && internal then "_"
else toString name |> addQuotes
where
addQuotes (n: String) :=
let quote := "\""
if n.contains Lean.idBeginEscape then s!"{quote}{n}{quote}" else n
/-- serialize a sort level. Expression is optimized to be compact e.g. `(+ u 2)` -/
partial def serializeSortLevel (level: Level) : String :=
let k := level.getOffset
let u := level.getLevelOffset
let u_str := match u with
| .zero => "0"
| .succ _ => panic! "getLevelOffset should not return .succ"
| .max v w =>
let v := serializeSortLevel v
let w := serializeSortLevel w
s!"(:max {v} {w})"
| .imax v w =>
let v := serializeSortLevel v
let w := serializeSortLevel w
s!"(:imax {v} {w})"
| .param name =>
s!"{name}"
| .mvar id =>
let name := id.name
s!"(:mv {name})"
match k, u with
| 0, _ => u_str
| _, .zero => s!"{k}"
| _, _ => s!"(+ {u_str} {k})"
/--
Completely serializes an expression tree. Json not used due to compactness
A `_` symbol in the AST indicates automatic deductions not present in the original expression.
-/
partial def serializeExpressionSexp (expr: Expr) : MetaM String := do
self expr
where
delayedMVarToSexp (e: Expr): MetaM (Option String) := do
let .some invocation ← toDelayedMVarInvocation e | return .none
let callee ← self $ .mvar invocation.mvarIdPending
let sites ← invocation.args.mapM (λ (fvarId, arg) => do
let arg := match arg with
| .some arg => arg
| .none => .fvar fvarId
self arg
)
let tailArgs ← invocation.tail.mapM self
let sites := " ".intercalate sites.toList
let result := if tailArgs.isEmpty then
s!"(:subst {callee} {sites})"
else
let tailArgs := " ".intercalate tailArgs.toList
s!"((:subst {callee} {sites}) {tailArgs})"
return .some result
self (e: Expr): MetaM String := do
if let .some result ← delayedMVarToSexp e then
return result
match e with
| .bvar deBruijnIndex =>
-- This is very common so the index alone is shown. Literals are handled below.
-- The raw de Bruijn index should never appear in an unbound setting. In
-- Lean these are handled using a `#` prefix.
pure s!"{deBruijnIndex}"
| .fvar fvarId =>
let name := fvarId.name
pure s!"(:fv {name})"
| .mvar mvarId => do
let pref := if ← mvarId.isDelayedAssigned then "mvd" else "mv"
let name := mvarId.name
pure s!"(:{pref} {name})"
| .sort level =>
let level := serializeSortLevel level
pure s!"(:sort {level})"
| .const declName _ =>
let declName := serializeName declName (sanitize := false)
-- The universe level of the const expression is elided since it should be
-- inferrable from surrounding expression
pure s!"(:c {declName})"
| .app _ _ => do
let fn' ← self e.getAppFn
let args := (← e.getAppArgs.mapM self) |>.toList
let args := " ".intercalate args
pure s!"({fn'} {args})"
| .lam binderName binderType body binderInfo => do
let binderName' := binderName.eraseMacroScopes
let binderType' ← self binderType
let body' ← self body
let binderInfo' := binderInfoSexp binderInfo
pure s!"(:lambda {binderName'} {binderType'} {body'}{binderInfo'})"
| .forallE binderName binderType body binderInfo => do
let binderName' := binderName.eraseMacroScopes
let binderType' ← self binderType
let body' ← self body
let binderInfo' := binderInfoSexp binderInfo
pure s!"(:forall {binderName'} {binderType'} {body'}{binderInfo'})"
| .letE name type value body _ => do
-- Dependent boolean flag diacarded
let name' := name.eraseMacroScopes
let type' ← self type
let value' ← self value
let body' ← self body
pure s!"(:let {name'} {type'} {value'} {body'})"
| .lit v =>
-- To not burden the downstream parser who needs to handle this, the literal
-- is wrapped in a :lit sexp.
let v' := match v with
| .natVal val => toString val
| .strVal val => IR.EmitC.quoteString val
pure s!"(:lit {v'})"
| .mdata _ inner =>
-- NOTE: Equivalent to expr itself, but mdata influences the prettyprinter
-- It may become necessary to incorporate the metadata.
self inner
| .proj _ _ _ => do
let env ← getEnv
let projApp := exprProjToApp env e
let autos := String.intercalate " " (List.replicate projApp.numParams "_")
let inner ← self projApp.inner
pure s!"((:c {projApp.projector}) {autos} {inner})"
-- Elides all unhygenic names
binderInfoSexp : Lean.BinderInfo → String
| .default => ""
| .implicit => " :i"
| .strictImplicit => " :si"
| .instImplicit => " :ii"
def serializeExpression (options: @&Protocol.Options) (e: Expr): MetaM Protocol.Expression := do
let pp?: Option String ← match options.printExprPretty with
| true => pure $ .some $ toString $ ← Meta.ppExpr e
| false => pure $ .none
let sexp?: Option String ← match options.printExprAST with
| true => pure $ .some $ ← serializeExpressionSexp e
| false => pure $ .none
let dependentMVars? ← match options.printDependentMVars with
| true => pure $ .some $ (← Meta.getMVars e).map (λ mvarId => mvarId.name.toString)
| false => pure $ .none
return {
pp?,
sexp?
dependentMVars?,
}
/-- Adapted from ppGoal -/
def serializeGoal (options: @&Protocol.Options) (goal: MVarId) (mvarDecl: MetavarDecl) (parentDecl?: Option MetavarDecl := .none)
: MetaM Protocol.Goal := do
-- Options for printing; See Meta.ppGoal for details
let showLetValues := true
let ppAuxDecls := options.printAuxDecls
let ppImplDetailHyps := options.printImplementationDetailHyps
let lctx := mvarDecl.lctx
let lctx := lctx.sanitizeNames.run' { options := (← getOptions) }
Meta.withLCtx lctx mvarDecl.localInstances do
let ppVarNameOnly (localDecl: LocalDecl): MetaM Protocol.Variable := do
match localDecl with
| .cdecl _ fvarId userName _ _ _ =>
return {
name := fvarId.name.toString,
userName:= ofName userName.simpMacroScopes,
isInaccessible := userName.isInaccessibleUserName
}
| .ldecl _ fvarId userName _ _ _ _ => do
return {
name := fvarId.name.toString,
userName := toString userName.simpMacroScopes,
isInaccessible := userName.isInaccessibleUserName
}
let ppVar (localDecl : LocalDecl) : MetaM Protocol.Variable := do
match localDecl with
| .cdecl _ fvarId userName type _ _ =>
let userName := userName.simpMacroScopes
let type ← instantiate type
return {
name := fvarId.name.toString,
userName:= ofName userName,
isInaccessible := userName.isInaccessibleUserName
type? := .some (← serializeExpression options type)
}
| .ldecl _ fvarId userName type val _ _ => do
let userName := userName.simpMacroScopes
let type ← instantiate type
let value? ← if showLetValues then
let val ← instantiate val
pure $ .some (← serializeExpression options val)
else
pure $ .none
return {
name := fvarId.name.toString,
userName:= ofName userName,
isInaccessible := userName.isInaccessibleUserName
type? := .some (← serializeExpression options type)
value? := value?
}
let vars ← lctx.foldlM (init := []) fun acc (localDecl : LocalDecl) => do
let skip := !ppAuxDecls && localDecl.isAuxDecl ||
!ppImplDetailHyps && localDecl.isImplementationDetail
if skip then
return acc
else
let nameOnly := options.noRepeat && (parentDecl?.map
(λ decl => decl.lctx.find? localDecl.fvarId |>.isSome) |>.getD false)
let var ← match nameOnly with
| true => ppVarNameOnly localDecl
| false => ppVar localDecl
return var::acc
return {
name := goal.name.toString,
userName? := if mvarDecl.userName == .anonymous then .none else .some (ofName mvarDecl.userName),
isConversion := isLHSGoal? mvarDecl.type |>.isSome,
target := (← serializeExpression options (← instantiate mvarDecl.type)),
vars := vars.reverse.toArray
}
where
instantiate := instantiateAll
ofName (n: Name) := serializeName n (sanitize := false)
protected def GoalState.serializeGoals
(state: GoalState)
(parent: Option GoalState := .none)
(options: @&Protocol.Options := {}):
MetaM (Array Protocol.Goal):= do
state.restoreMetaM
let goals := state.goals.toArray
let parentDecl? := parent.bind (λ parentState => parentState.mctx.findDecl? state.parentMVar?.get!)
goals.mapM fun goal => do
match state.mctx.findDecl? goal with
| .some mvarDecl =>
let serializedGoal ← serializeGoal options goal mvarDecl (parentDecl? := parentDecl?)
pure serializedGoal
| .none => throwError s!"Metavariable does not exist in context {goal.name}"
/-- Print the metavariables in a readable format -/
@[export pantograph_goal_state_diag_m]
protected def GoalState.diag (goalState: GoalState) (parent?: Option GoalState := .none) (options: Protocol.GoalDiag := {}): CoreM String := do
let metaM: MetaM String := do
goalState.restoreMetaM
let savedState := goalState.savedState
let goals := savedState.tactic.goals
let mctx ← getMCtx
let root := goalState.root
-- Print the root
let result: String ← match mctx.decls.find? root with
| .some decl => printMVar ">" root decl
| .none => pure s!">{root.name}: ??"
let resultGoals ← goals.filter (· != root) |>.mapM (fun mvarId =>
match mctx.decls.find? mvarId with
| .some decl => printMVar "⊢" mvarId decl
| .none => pure s!"⊢{mvarId.name}: ??"
)
let goals := goals.toSSet
let resultOthers ← mctx.decls.toList.filter (λ (mvarId, _) =>
!(goals.contains mvarId || mvarId == root) && options.printAll)
|>.mapM (fun (mvarId, decl) => do
let pref := if parentHasMVar mvarId then " " else "~"
printMVar pref mvarId decl
)
pure $ result ++ "\n" ++ (resultGoals.map (· ++ "\n") |> String.join) ++ (resultOthers.map (· ++ "\n") |> String.join)
metaM.run' {}
where
printMVar (pref: String) (mvarId: MVarId) (decl: MetavarDecl): MetaM String := mvarId.withContext do
let resultFVars: List String ←
if options.printContext then
decl.lctx.fvarIdToDecl.toList.mapM (λ (fvarId, decl) =>
do pure $ (← printFVar fvarId decl) ++ "\n")
else
pure []
let type ← if options.instantiate
then instantiateAll decl.type
else pure $ decl.type
let type_sexp ← if options.printSexp then
let sexp ← serializeExpressionSexp type
pure <| " " ++ sexp
else
pure ""
let resultMain: String := s!"{pref}{mvarId.name}{userNameToString decl.userName}: {← Meta.ppExpr decl.type}{type_sexp}"
let resultValue: String ←
if options.printValue then
if let .some value ← getExprMVarAssignment? mvarId then
let value ← if options.instantiate
then instantiateAll value
else pure $ value
pure s!"\n := {← Meta.ppExpr value}"
else if let .some { mvarIdPending, .. } ← getDelayedMVarAssignment? mvarId then
pure s!"\n ::= {mvarIdPending.name}"
else
pure ""
else
pure ""
pure $ (String.join resultFVars) ++ resultMain ++ resultValue
printFVar (fvarId: FVarId) (decl: LocalDecl): MetaM String := do
pure s!" | {fvarId.name}{userNameToString decl.userName}: {← Meta.ppExpr decl.type}"
userNameToString : Name → String
| .anonymous => ""
| other => s!"[{other}]"
parentHasMVar (mvarId: MVarId): Bool := parent?.map (λ state => state.mctx.decls.contains mvarId) |>.getD true
end Pantograph

40
Pantograph/Elab.lean Normal file
View File

@ -0,0 +1,40 @@
import Lean
open Lean
namespace Pantograph
-- Functions for creating contexts and states
@[export pantograph_default_elab_context]
def defaultElabContext: Elab.Term.Context := {
errToSorry := false
}
/-- Read syntax object from string -/
def parseTerm (env: Environment) (s: String): Except String Syntax :=
Parser.runParserCategory
(env := env)
(catName := `term)
(input := s)
(fileName := "<stdin>")
def parseTermM [Monad m] [MonadEnv m] (s: String): m (Except String Syntax) := do
return Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := s)
(fileName := "<stdin>")
/-- Parse a syntax object. May generate additional metavariables! -/
def elabType (syn: Syntax): Elab.TermElabM (Except String Expr) := do
try
let expr ← Elab.Term.elabType syn
return .ok expr
catch ex => return .error (← ex.toMessageData.toString)
def elabTerm (syn: Syntax) (expectedType? : Option Expr := .none): Elab.TermElabM (Except String Expr) := do
try
let expr ← Elab.Term.elabTerm (stx := syn) expectedType?
return .ok expr
catch ex => return .error (← ex.toMessageData.toString)
end Pantograph

170
Pantograph/Environment.lean Normal file
View File

@ -0,0 +1,170 @@
import Pantograph.Delate
import Pantograph.Elab
import Pantograph.Protocol
import Pantograph.Serial
import Lean.Environment
import Lean.Replay
open Lean
open Pantograph
namespace Pantograph.Environment
@[export pantograph_is_name_internal]
def isNameInternal (n: Name): Bool :=
-- Returns true if the name is an implementation detail which should not be shown to the user.
n.isAuxLemma n.hasMacroScopes
/-- Catalog all the non-internal and safe names -/
@[export pantograph_environment_catalog]
def env_catalog (env: Environment): Array Name := env.constants.fold (init := #[]) (λ acc name _ =>
match isNameInternal name with
| false => acc.push name
| true => acc)
@[export pantograph_environment_module_of_name]
def module_of_name (env: Environment) (name: Name): Option Name := do
let moduleId ← env.getModuleIdxFor? name
return env.allImportedModuleNames.get! moduleId.toNat
def toCompactSymbolName (n: Name) (info: ConstantInfo): String :=
let pref := match info with
| .axiomInfo _ => "a"
| .defnInfo _ => "d"
| .thmInfo _ => "t"
| .opaqueInfo _ => "o"
| .quotInfo _ => "q"
| .inductInfo _ => "i"
| .ctorInfo _ => "c"
| .recInfo _ => "r"
s!"{pref}{toString n}"
def toFilteredSymbol (n: Lean.Name) (info: Lean.ConstantInfo): Option String :=
if isNameInternal n || info.isUnsafe
then Option.none
else Option.some <| toCompactSymbolName n info
def catalog (_: Protocol.EnvCatalog): CoreM Protocol.EnvCatalogResult := do
let env ← Lean.MonadEnv.getEnv
let names := env.constants.fold (init := #[]) (λ acc name info =>
match toFilteredSymbol name info with
| .some x => acc.push x
| .none => acc)
return { symbols := names }
def inspect (args: Protocol.EnvInspect) (options: @&Protocol.Options): CoreM (Protocol.CR Protocol.EnvInspectResult) := do
let env ← Lean.MonadEnv.getEnv
let name := args.name.toName
let info? := env.find? name
let info ← match info? with
| none => return .error $ Protocol.errorIndex s!"Symbol not found {args.name}"
| some info => pure info
let module? := env.getModuleIdxFor? name >>=
(λ idx => env.allImportedModuleNames.get? idx.toNat)
let value? := match args.value?, info with
| .some true, _ => info.value?
| .some false, _ => .none
| .none, .defnInfo _ => info.value?
| .none, _ => .none
let type ← unfoldAuxLemmas info.type
let value? ← value?.mapM (λ v => unfoldAuxLemmas v)
-- Information common to all symbols
let core := {
type := ← (serializeExpression options type).run',
isUnsafe := info.isUnsafe,
value? := ← value?.mapM (λ v => serializeExpression options v |>.run'),
publicName? := Lean.privateToUserName? name |>.map (·.toString),
-- BUG: Warning: getUsedConstants here will not include projections. This is a known bug.
typeDependency? := if args.dependency?.getD false
then .some <| type.getUsedConstants.map (λ n => serializeName n)
else .none,
valueDependency? := if args.dependency?.getD false
then value?.map (λ e =>
e.getUsedConstants.filter (!isNameInternal ·) |>.map (λ n => serializeName n) )
else .none,
module? := module?.map (·.toString)
}
let result ← match info with
| .inductInfo induct => pure { core with inductInfo? := .some {
numParams := induct.numParams,
numIndices := induct.numIndices,
all := induct.all.toArray.map (·.toString),
ctors := induct.ctors.toArray.map (·.toString),
isRec := induct.isRec,
isReflexive := induct.isReflexive,
isNested := induct.isNested,
} }
| .ctorInfo ctor => pure { core with constructorInfo? := .some {
induct := ctor.induct.toString,
cidx := ctor.cidx,
numParams := ctor.numParams,
numFields := ctor.numFields,
} }
| .recInfo r => pure { core with recursorInfo? := .some {
all := r.all.toArray.map (·.toString),
numParams := r.numParams,
numIndices := r.numIndices,
numMotives := r.numMotives,
numMinors := r.numMinors,
rules := ← r.rules.toArray.mapM (λ rule => do
pure {
ctor := rule.ctor.toString,
nFields := rule.nfields,
rhs := ← (serializeExpression options rule.rhs).run',
})
k := r.k,
} }
| _ => pure core
let result ← if args.source?.getD false then
let srcSearchPath ← initSrcSearchPath
let sourceUri? ← module?.bindM (Server.documentUriFromModule srcSearchPath ·)
let declRange? ← findDeclarationRanges? name
let sourceStart? := declRange?.map (·.range.pos)
let sourceEnd? := declRange?.map (·.range.endPos)
.pure {
result with
sourceUri?,
sourceStart?,
sourceEnd?,
}
else
.pure result
return .ok result
def addDecl (args: Protocol.EnvAdd): CoreM (Protocol.CR Protocol.EnvAddResult) := do
let env ← Lean.MonadEnv.getEnv
let tvM: Elab.TermElabM (Except String (Expr × Expr)) := do
let type ← match parseTerm env args.type with
| .ok syn => do
match ← elabTerm syn with
| .error e => return .error e
| .ok expr => pure expr
| .error e => return .error e
let value ← match parseTerm env args.value with
| .ok syn => do
try
let expr ← Elab.Term.elabTerm (stx := syn) (expectedType? := .some type)
Lean.Elab.Term.synthesizeSyntheticMVarsNoPostponing
let expr ← instantiateMVars expr
pure $ expr
catch ex => return .error (← ex.toMessageData.toString)
| .error e => return .error e
pure $ .ok (type, value)
let (type, value) ← match ← tvM.run' (ctx := {}) |>.run' with
| .ok t => pure t
| .error e => return .error $ Protocol.errorExpr e
let constant := Lean.Declaration.defnDecl <| Lean.mkDefinitionValEx
(name := args.name.toName)
(levelParams := [])
(type := type)
(value := value)
(hints := Lean.mkReducibilityHintsRegularEx 1)
(safety := Lean.DefinitionSafety.safe)
(all := [])
let env' ← match env.addDecl (← getOptions) constant with
| .error e => do
let options ← Lean.MonadOptions.getOptions
let desc ← (e.toMessageData options).toString
return .error $ { error := "kernel", desc }
| .ok env' => pure env'
Lean.MonadEnv.modifyEnv (λ _ => env')
return .ok {}
end Pantograph.Environment

4
Pantograph/Frontend.lean Normal file
View File

@ -0,0 +1,4 @@
import Pantograph.Frontend.Basic
import Pantograph.Frontend.Elab
import Pantograph.Frontend.InfoTree
import Pantograph.Frontend.MetaTranslate

View File

@ -0,0 +1,127 @@
import Lean.Parser
import Lean.Elab.Frontend
open Lean
namespace Lean.FileMap
/-- Extract the range of a `Syntax` expressed as lines and columns. -/
-- Extracted from the private declaration `Lean.Elab.formatStxRange`,
-- in `Lean.Elab.InfoTree.Main`.
@[export pantograph_frontend_stx_range]
protected def stxRange (fileMap : FileMap) (stx : Syntax) : Position × Position :=
let pos := stx.getPos?.getD 0
let endPos := stx.getTailPos?.getD pos
(fileMap.toPosition pos, fileMap.toPosition endPos)
end Lean.FileMap
namespace Lean.PersistentArray
/--
Drop the first `n` elements of a `PersistentArray`, returning the results as a `List`.
-/
-- We can't remove the `[Inhabited α]` hypotheses here until
-- `PersistentArray`'s `GetElem` instance also does.
protected def drop [Inhabited α] (t : PersistentArray α) (n : Nat) : List α :=
List.range (t.size - n) |>.map fun i => t.get! (n + i)
end Lean.PersistentArray
namespace Pantograph.Frontend
@[export pantograph_frontend_stx_byte_range]
def stxByteRange (stx : Syntax) : String.Pos × String.Pos :=
let pos := stx.getPos?.getD 0
let endPos := stx.getTailPos?.getD 0
(pos, endPos)
abbrev FrontendM := Elab.Frontend.FrontendM
structure CompilationStep where
fileName : String
fileMap : FileMap
src : Substring
stx : Syntax
before : Environment
after : Environment
msgs : List Message
trees : List Elab.InfoTree
namespace CompilationStep
@[export pantograph_frontend_compilation_step_message_strings_m]
def messageStrings (step: CompilationStep) : IO (Array String) := do
List.toArray <$> step.msgs.mapM (·.toString)
end CompilationStep
/--
Process one command, returning a `CompilationStep` and
`done : Bool`, indicating whether this was the last command.
-/
@[export pantograph_frontend_process_one_command_m]
def processOneCommand: FrontendM (CompilationStep × Bool) := do
let s := (← get).commandState
let before := s.env
let done ← Elab.Frontend.processCommand
let stx := (← get).commands.back!
let src := (← read).inputCtx.input.toSubstring.extract (← get).cmdPos (← get).parserState.pos
let s' := (← get).commandState
let after := s'.env
let msgs := s'.messages.toList.drop s.messages.toList.length
let trees := s'.infoState.trees.drop s.infoState.trees.size
let ⟨_, fileName, fileMap⟩ := (← read).inputCtx
return ({ fileName, fileMap, src, stx, before, after, msgs, trees }, done)
partial def mapCompilationSteps { α } (f: CompilationStep → IO α) : FrontendM (List α) := do
let (cmd, done) ← processOneCommand
if done then
if cmd.src.isEmpty then
return []
else
return [← f cmd]
else
return (← f cmd) :: (← mapCompilationSteps f)
@[export pantograph_frontend_find_source_path_m]
def findSourcePath (module : Name) : IO System.FilePath := do
return System.FilePath.mk ((← findOLean module).toString.replace ".lake/build/lib/" "") |>.withExtension "lean"
/--
Use with
```lean
let m: FrontendM α := ...
let (context, state) ← createContextStateFromFile ...
m.run context |>.run' state
```
-/
@[export pantograph_frontend_create_context_state_from_file_m]
def createContextStateFromFile
(file : String) -- Content of the file
(fileName : String := "<anonymous>")
(env? : Option Lean.Environment := .none) -- If set to true, assume there's no header.
(opts : Options := {})
: IO (Elab.Frontend.Context × Elab.Frontend.State) := unsafe do
--let file ← IO.FS.readFile (← findSourcePath module)
let inputCtx := Parser.mkInputContext file fileName
let (env, parserState, messages) ← match env? with
| .some env => pure (env, {}, .empty)
| .none =>
let (header, parserState, messages) ← Parser.parseHeader inputCtx
let (env, messages) ← Elab.processHeader header opts messages inputCtx
pure (env, parserState, messages)
let commandState := Elab.Command.mkState env messages opts
let context: Elab.Frontend.Context := { inputCtx }
let state: Elab.Frontend.State := {
commandState := { commandState with infoState.enabled := true },
parserState,
cmdPos := parserState.pos
}
return (context, state)
end Pantograph.Frontend

View File

@ -0,0 +1,194 @@
import Lean.Elab.Import
import Lean.Elab.Command
import Lean.Elab.InfoTree
import Lean.DeclarationRange
import Pantograph.Frontend.Basic
import Pantograph.Frontend.MetaTranslate
import Pantograph.Goal
import Pantograph.Protocol
import Pantograph.Frontend.InfoTree
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 ppExpr (t : TacticInvocation) (e : Expr) : IO Format :=
t.runMetaM (fun _ => do Meta.ppExpr (← instantiateMVars e))
protected def usedConstants (t: TacticInvocation) : NameSet :=
let info := t.info
info.goalsBefore
|>.filterMap info.mctxAfter.getExprAssignmentCore?
|>.map Expr.getUsedConstantsAsSet
|>.foldl .union .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.map λ n => n.toString
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`.
-/
@[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 }
@[export pantograph_frontend_collect_new_defined_constants_m]
def collectNewDefinedConstants (step : CompilationStep) : IO (List Name) := do
step.after.constants.map₂.foldlM (λ acc name _ => do
if step.before.contains name then
return acc
let coreM : CoreM Bool := Option.isSome <$> findDeclarationRanges? name
let hasRange ← coreM.run' { fileName := step.fileName, fileMap := step.fileMap } { env := step.after } |>.toBaseIO
match hasRange with
| .ok true => return name :: acc
| .ok false => return acc
| .error e => throw $ IO.userError (← e.toMessageData.toString)
) []
end Pantograph.Frontend

View File

@ -0,0 +1,157 @@
/- Adapted from lean-training-data -/
import Lean.Elab.InfoTree
import Lean.Parser.Term
import Lean.PrettyPrinter
open Lean
namespace Lean.Elab
private def elaboratorToString : Name → String
| .anonymous => ""
| n => s!"⟨{n}⟩ "
private def indent (s : String) : String := "\n".intercalate $ s.splitOn "\n" |>.map ("\t" ++ .)
/-- The `Syntax` for a `Lean.Elab.Info`, if there is one. -/
protected def Info.stx? : Info → Option Syntax
| .ofTacticInfo info => info.stx
| .ofTermInfo info => info.stx
| .ofCommandInfo info => info.stx
| .ofMacroExpansionInfo info => info.stx
| .ofOptionInfo info => info.stx
| .ofFieldInfo info => info.stx
| .ofCompletionInfo info => info.stx
| .ofUserWidgetInfo info => info.stx
| .ofCustomInfo info => info.stx
| .ofFVarAliasInfo _ => none
| .ofFieldRedeclInfo info => info.stx
| .ofOmissionInfo info => info.stx
| .ofChoiceInfo info => info.stx
| .ofPartialTermInfo info => info.stx
/-- Is the `Syntax` for this `Lean.Elab.Info` original, or synthetic? -/
protected def Info.isOriginal (i : Info) : Bool :=
match i.stx? with
| none => true -- Somewhat unclear what to do with `FVarAliasInfo`, so be conservative.
| some stx => match stx.getHeadInfo with
| .original .. => true
| _ => false
def ContextInfo.ppExpr (ctx : ContextInfo) (lctx : LocalContext) (e : Expr) : IO Format :=
ctx.runMetaM lctx (do Meta.ppExpr (← instantiateMVars e))
def CommandInfo.toString (info : CommandInfo) (ctx : ContextInfo) : IO String := do
let stx := (← ctx.ppSyntax {} info.stx).pretty
return s!"{elaboratorToString info.elaborator}\n{stx}"
def TermInfo.toString (info : TermInfo) (ctx : ContextInfo) : IO String := do
let stx := (← ctx.ppSyntax info.lctx info.stx).pretty
let expectedType := (← info.expectedType?.mapM fun ty => do
pure s!": {(← ctx.ppExpr info.lctx ty).pretty}").getD ""
let expr := (← ctx.ppExpr info.lctx info.expr).pretty
return s!"{elaboratorToString info.elaborator}{expr}{expectedType}\n{stx}"
/-- Find the name for the outermost `Syntax` in this `TacticInfo`. -/
def TacticInfo.name? (t : TacticInfo) : Option Name :=
match t.stx with
| Syntax.node _ n _ => some n
| _ => none
/-- Decide whether a tactic is "substantive",
or is merely a tactic combinator (e.g. `by`, `;`, multiline tactics, parenthesized tactics). -/
def TacticInfo.isSubstantive (t : TacticInfo) : Bool :=
match t.name? with
| none => false
| some `null => false
| some ``cdot => false
| some ``cdotTk => false
| some ``Lean.Parser.Term.byTactic => false
| some ``Lean.Parser.Tactic.tacticSeq => false
| some ``Lean.Parser.Tactic.tacticSeq1Indented => false
| some ``Lean.Parser.Tactic.«tactic_<;>_» => false
| some ``Lean.Parser.Tactic.paren => false
| _ => true
def TacticInfo.pp (info : TacticInfo) (ctx : ContextInfo) : IO Format :=
ctx.runMetaM {} try
Lean.PrettyPrinter.ppTactic ⟨info.stx⟩
catch _ =>
pure "<failed to pretty print>"
def TacticInfo.toString (i : TacticInfo) (ctx : ContextInfo) : IO String := do
let name := i.name?
let stx := Format.pretty (← i.pp ctx)
return s!"{name}\n{stx}"
/--
Keep `.node` nodes and `.hole` nodes satisfying predicates.
Returns a `List InfoTree`, although in most situations this will be a singleton.
-/
partial def InfoTree.filter (p : Info → Bool) (m : MVarId → Bool := fun _ => false) :
InfoTree → List InfoTree
| .context ctx tree => tree.filter p m |>.map (.context ctx)
| .node info children =>
if p info then
[.node info (children.toList.map (filter p m)).flatten.toPArray']
else
(children.toList.map (filter p m)).flatten
| .hole mvar => if m mvar then [.hole mvar] else []
/-- Analogue of `Lean.Elab.InfoTree.findInfo?`, but that returns a list of all results. -/
partial def InfoTree.findAllInfo
(t : InfoTree)
(context?: Option Elab.ContextInfo)
(haltOnMatch : Bool := false)
(pred : Elab.Info → Bool)
: List (Elab.Info × Option Elab.ContextInfo × PersistentArray Elab.InfoTree) :=
match t with
| .context inner t => findAllInfo t (inner.mergeIntoOuter? context?) haltOnMatch pred
| .node i children =>
let head := if pred i then [(i, context?, children)] else []
let tail := if haltOnMatch ∧ !head.isEmpty then [] else children.toList.flatMap (fun t => findAllInfo t context? haltOnMatch pred)
head ++ tail
| _ => []
/-- Monadic analogue of `findAllInfo`, but predicate controls whether to recurse. -/
partial def InfoTree.findAllInfoM [Monad m]
(t : InfoTree)
(context?: Option Elab.ContextInfo)
(pred : Elab.Info → Option Elab.ContextInfo → m (Bool × Bool))
: m (List (Elab.Info × Option Elab.ContextInfo × PersistentArray Elab.InfoTree)) := do
match t with
| .context inner t => t.findAllInfoM (inner.mergeIntoOuter? context?) pred
| .node i children =>
let (flagCollect, flagRecurse) ← pred i context?
let head := if flagCollect then [(i, context?, children)] else []
let tail := if ¬ flagRecurse then pure [] else children.toList.mapM (fun t => t.findAllInfoM context? pred)
return head ++ (← tail).flatten
| _ => return []
@[export pantograph_infotree_to_string_m]
partial def InfoTree.toString (t : InfoTree) (ctx?: Option Elab.ContextInfo := .none) : IO String := do
match t with
| .context ctx t => t.toString (ctx.mergeIntoOuter? ctx?)
| .node info children =>
if let some ctx := ctx? then
let node : String ← match info with
| .ofTermInfo info => pure s!"[term] {(← info.toString ctx)}"
| .ofCommandInfo info => pure s!"[command] {(← info.toString ctx)}"
| .ofTacticInfo info => pure s!"[tactic] {(← info.toString ctx)}"
| .ofMacroExpansionInfo _ => pure "[macro_exp]"
| .ofOptionInfo _ => pure "[option]"
| .ofFieldInfo _ => pure "[field]"
| .ofCompletionInfo _ => pure "[completion]"
| .ofUserWidgetInfo _ => pure "[user_widget]"
| .ofCustomInfo _ => pure "[custom]"
| .ofFVarAliasInfo _ => pure "[fvar]"
| .ofFieldRedeclInfo _ => pure "[field_redecl]"
| .ofOmissionInfo _ => pure "[omission]"
| .ofChoiceInfo _ => pure "[choice]"
| .ofPartialTermInfo _ => pure "[partial_term]"
let children := "\n".intercalate (← children.toList.mapM λ t' => do pure $ indent $ ← t'.toString ctx)
return s!"{node}\n{children}"
else throw <| IO.userError "No `ContextInfo` available."
| .hole mvarId =>
if let some ctx := ctx? then
let payload := (← ctx.runMetaM {} (do Meta.ppGoal mvarId)).pretty
return s!"[hole] {payload}"
else throw <| IO.userError "No `ContextInfo` available."
end Lean.Elab

View File

@ -0,0 +1,165 @@
import Lean.Meta
import Std.Data.HashMap
open Lean
namespace Pantograph.Frontend
namespace MetaTranslate
structure Context where
sourceMCtx : MetavarContext := {}
sourceLCtx : LocalContext := {}
abbrev FVarMap := Std.HashMap FVarId FVarId
structure State where
-- Stores mapping from old to new mvar/fvars
mvarMap: Std.HashMap MVarId MVarId := {}
fvarMap: Std.HashMap FVarId FVarId := {}
/-
Monadic state for translating a frozen meta state. The underlying `MetaM`
operates in the "target" context and state.
-/
abbrev MetaTranslateM := ReaderT Context StateRefT State MetaM
def getSourceLCtx : MetaTranslateM LocalContext := do pure (← read).sourceLCtx
def getSourceMCtx : MetaTranslateM MetavarContext := do pure (← read).sourceMCtx
def addTranslatedFVar (src dst: FVarId) : MetaTranslateM Unit := do
modifyGet λ state => ((), { state with fvarMap := state.fvarMap.insert src dst })
def addTranslatedMVar (src dst: MVarId) : MetaTranslateM Unit := do
modifyGet λ state => ((), { state with mvarMap := state.mvarMap.insert src dst })
def saveFVarMap : MetaTranslateM FVarMap := do
return (← get).fvarMap
def restoreFVarMap (map: FVarMap) : MetaTranslateM Unit := do
modifyGet λ state => ((), { state with fvarMap := map })
def resetFVarMap : MetaTranslateM Unit := do
modifyGet λ state => ((), { state with fvarMap := {} })
mutual
private partial def translateLevel (srcLevel: Level) : MetaTranslateM Level := do
let sourceMCtx ← getSourceMCtx
let (_, level) := instantiateLevelMVarsImp sourceMCtx srcLevel
match level with
| .zero => return .zero
| .succ inner => do
let inner' ← translateLevel inner
return .succ inner'
| .max l1 l2 => do
let l1' ← translateLevel l1
let l2' ← translateLevel l2
return .max l1' l2'
| .imax l1 l2 => do
let l1' ← translateLevel l1
let l2' ← translateLevel l2
return .imax l1' l2'
| .param p => return .param p
| .mvar _ =>
Meta.mkFreshLevelMVar
private partial def translateExpr (srcExpr: Expr) : MetaTranslateM Expr := do
let sourceMCtx ← getSourceMCtx
-- We want to create as few mvars as possible
let (srcExpr, _) := instantiateMVarsCore (mctx := sourceMCtx) srcExpr
--IO.println s!"Transform src: {srcExpr}"
let result ← Core.transform srcExpr λ e => do
let state ← get
match e with
| .fvar fvarId =>
let .some fvarId' := state.fvarMap[fvarId]? | panic! s!"FVar id not registered: {fvarId.name}"
-- Delegating this to `Meta.check` later
--assert! (← getLCtx).contains fvarId'
return .done $ .fvar fvarId'
| .mvar mvarId => do
-- Must not be assigned
assert! !(sourceMCtx.eAssignment.contains mvarId)
match state.mvarMap[mvarId]? with
| .some mvarId' => do
return .done $ .mvar mvarId'
| .none => do
-- Entering another LCtx, must save the current one
let fvarMap ← saveFVarMap
let mvarId' ← translateMVarId mvarId
restoreFVarMap fvarMap
return .done $ .mvar mvarId'
| .sort level => do
let level' ← translateLevel level
return .done $ .sort level'
| _ => return .continue
Meta.check result
return result
partial def translateLocalInstance (srcInstance: LocalInstance) : MetaTranslateM LocalInstance := do
return {
className := srcInstance.className,
fvar := ← translateExpr srcInstance.fvar
}
partial def translateLocalDecl (srcLocalDecl: LocalDecl) : MetaTranslateM LocalDecl := do
let fvarId ← mkFreshFVarId
addTranslatedFVar srcLocalDecl.fvarId fvarId
match srcLocalDecl with
| .cdecl index _ userName type bi kind => do
--IO.println s!"[CD] {userName} {toString type}"
return .cdecl index fvarId userName (← translateExpr type) bi kind
| .ldecl index _ userName type value nonDep kind => do
--IO.println s!"[LD] {toString type} := {toString value}"
return .ldecl index fvarId userName (← translateExpr type) (← translateExpr value) nonDep kind
partial def translateLCtx : MetaTranslateM LocalContext := do
resetFVarMap
let lctx ← MonadLCtx.getLCtx
assert! lctx.isEmpty
(← getSourceLCtx).foldlM (λ lctx srcLocalDecl => do
let localDecl ← Meta.withLCtx lctx #[] do
translateLocalDecl srcLocalDecl
pure $ lctx.addDecl localDecl
) lctx
partial def translateMVarId (srcMVarId: MVarId) : MetaTranslateM MVarId := do
if let .some mvarId' := (← get).mvarMap[srcMVarId]? then
return mvarId'
let mvarId' ← Meta.withLCtx .empty #[] do
let srcDecl := (← getSourceMCtx).findDecl? srcMVarId |>.get!
withTheReader Context (λ ctx => { ctx with sourceLCtx := srcDecl.lctx }) do
let lctx' ← translateLCtx
let localInstances' ← srcDecl.localInstances.mapM translateLocalInstance
Meta.withLCtx lctx' localInstances' do
let target' ← translateExpr srcDecl.type
let mvar' ← Meta.mkFreshExprMVar target' srcDecl.kind srcDecl.userName
let mvarId' := mvar'.mvarId!
if let .some { fvars, mvarIdPending }:= (← getSourceMCtx).getDelayedMVarAssignmentExp srcMVarId then
-- Map the fvars in the pending context.
let mvarIdPending' ← translateMVarId mvarIdPending
let fvars' ← mvarIdPending'.withContext $ fvars.mapM translateExpr
assignDelayedMVar mvarId' fvars' mvarIdPending'
pure mvarId'
addTranslatedMVar srcMVarId mvarId'
return mvarId'
end
def translateMVarFromTermInfo (termInfo : Elab.TermInfo) (context? : Option Elab.ContextInfo)
: MetaTranslateM MVarId := do
withTheReader Context (λ ctx => { ctx with
sourceMCtx := context?.map (·.mctx) |>.getD {},
sourceLCtx := termInfo.lctx,
}) do
let type := termInfo.expectedType?.get!
let lctx' ← translateLCtx
let mvar ← Meta.withLCtx lctx' #[] do
let type' ← translateExpr type
Meta.mkFreshExprSyntheticOpaqueMVar type'
return mvar.mvarId!
def translateMVarFromTacticInfoBefore (tacticInfo : Elab.TacticInfo) (_context? : Option Elab.ContextInfo)
: MetaTranslateM (List MVarId) := do
withTheReader Context (λ ctx => { ctx with sourceMCtx := tacticInfo.mctxBefore }) do
tacticInfo.goalsBefore.mapM translateMVarId
end MetaTranslate
export MetaTranslate (MetaTranslateM)
end Pantograph.Frontend

454
Pantograph/Goal.lean Normal file
View File

@ -0,0 +1,454 @@
/-
Functions for handling metavariables
All the functions starting with `try` resume their inner monadic state.
-/
import Pantograph.Tactic
import Lean
namespace Pantograph
open Lean
/--
Represents an interconnected set of metavariables, or a state in proof search
-/
structure GoalState where
savedState : Elab.Tactic.SavedState
-- The root hole which is the search target
root: MVarId
-- Parent state metavariable source
parentMVar?: Option MVarId
-- Existence of this field shows that we are currently in `conv` mode.
-- (convRhs, goal, dormant)
convMVar?: Option (MVarId × MVarId × List MVarId) := .none
-- Previous RHS for calc, so we don't have to repeat it every time
-- WARNING: If using `state with` outside of `calc`, this must be set to `.none`
calcPrevRhs?: Option (MVarId × Expr) := .none
@[export pantograph_goal_state_create_m]
protected def GoalState.create (expr: Expr): Elab.TermElabM GoalState := do
-- May be necessary to immediately synthesise all metavariables if we need to leave the elaboration context.
-- See https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/Unknown.20universe.20metavariable/near/360130070
--Elab.Term.synthesizeSyntheticMVarsNoPostponing
--let expr ← instantiateMVars expr
let root ← Meta.mkFreshExprMVar expr (kind := MetavarKind.synthetic) (userName := .anonymous)
let savedStateMonad: Elab.Tactic.TacticM Elab.Tactic.SavedState := MonadBacktrack.saveState
let savedState ← savedStateMonad { elaborator := .anonymous } |>.run' { goals := [root.mvarId!]}
return {
root := root.mvarId!,
savedState,
parentMVar? := .none,
}
@[export pantograph_goal_state_create_from_mvars_m]
protected def GoalState.createFromMVars (goals: List MVarId) (root: MVarId): MetaM GoalState := do
let savedStateMonad: Elab.Tactic.TacticM Elab.Tactic.SavedState := MonadBacktrack.saveState
let savedState ← savedStateMonad { elaborator := .anonymous } |>.run' { goals } |>.run' {}
return {
root,
savedState,
parentMVar? := .none,
}
@[export pantograph_goal_state_is_conv]
protected def GoalState.isConv (state: GoalState): Bool :=
state.convMVar?.isSome
protected def GoalState.goals (state: GoalState): List MVarId :=
state.savedState.tactic.goals
@[export pantograph_goal_state_goals]
protected def GoalState.goalsArray (state: GoalState): Array MVarId := state.goals.toArray
protected def GoalState.mctx (state: GoalState): MetavarContext :=
state.savedState.term.meta.meta.mctx
protected def GoalState.env (state: GoalState): Environment :=
state.savedState.term.meta.core.env
@[export pantograph_goal_state_meta_context_of_goal]
protected def GoalState.metaContextOfGoal (state: GoalState) (mvarId: MVarId): Option Meta.Context := do
let mvarDecl ← state.mctx.findDecl? mvarId
return { lctx := mvarDecl.lctx, localInstances := mvarDecl.localInstances }
protected def GoalState.metaState (state: GoalState): Meta.State :=
state.savedState.term.meta.meta
protected def GoalState.coreState (state: GoalState): Core.SavedState :=
state.savedState.term.meta.core
protected def GoalState.withContext (state: GoalState) (mvarId: MVarId) (m: MetaM α): MetaM α := do
mvarId.withContext m |>.run' (← read) state.metaState
protected def GoalState.withParentContext { n } [MonadControlT MetaM n] [Monad n] (state: GoalState): n α → n α :=
Meta.mapMetaM <| state.withContext state.parentMVar?.get!
protected def GoalState.withRootContext { n } [MonadControlT MetaM n] [Monad n] (state: GoalState): n α → n α :=
Meta.mapMetaM <| state.withContext state.root
private def GoalState.mvars (state: GoalState): SSet MVarId :=
state.mctx.decls.foldl (init := .empty) fun acc k _ => acc.insert k
protected def GoalState.restoreMetaM (state: GoalState): MetaM Unit :=
state.savedState.term.meta.restore
protected def GoalState.restoreElabM (state: GoalState): Elab.TermElabM Unit :=
state.savedState.term.restore
private def GoalState.restoreTacticM (state: GoalState) (goal: MVarId): Elab.Tactic.TacticM Unit := do
state.savedState.restore
Elab.Tactic.setGoals [goal]
@[export pantograph_goal_state_focus]
protected def GoalState.focus (state: GoalState) (goalId: Nat): Option GoalState := do
let goal ← state.savedState.tactic.goals.get? goalId
return {
state with
savedState := {
state.savedState with
tactic := { goals := [goal] },
},
calcPrevRhs? := .none,
}
/-- Immediately bring all parent goals back into scope. Used in automatic mode -/
@[export pantograph_goal_state_immediate_resume_parent]
protected def GoalState.immediateResume (state: GoalState) (parent: GoalState): GoalState :=
-- Prune parents solved goals
let mctx := state.mctx
let parentGoals := parent.goals.filter $ λ goal => mctx.eAssignment.contains goal
{
state with
savedState := {
state.savedState with
tactic := { goals := state.goals ++ parentGoals },
},
}
/--
Brings into scope a list of goals
-/
@[export pantograph_goal_state_resume]
protected def GoalState.resume (state: GoalState) (goals: List MVarId): Except String GoalState :=
if ¬ (goals.all (λ goal => state.mvars.contains goal)) then
let invalid_goals := goals.filter (λ goal => ¬ state.mvars.contains goal) |>.map (·.name.toString)
.error s!"Goals {invalid_goals} are not in scope"
else
-- Set goals to the goals that have not been assigned yet, similar to the `focus` tactic.
let unassigned := goals.filter (λ goal =>
let mctx := state.mctx
¬(mctx.eAssignment.contains goal || mctx.dAssignment.contains goal))
.ok {
state with
savedState := {
term := state.savedState.term,
tactic := { goals := unassigned },
},
}
/--
Brings into scope all goals from `branch`
-/
@[export pantograph_goal_state_continue]
protected def GoalState.continue (target: GoalState) (branch: GoalState): Except String GoalState :=
if !target.goals.isEmpty then
.error s!"Target state has unresolved goals"
else if target.root != branch.root then
.error s!"Roots of two continued goal states do not match: {target.root.name} != {branch.root.name}"
else
target.resume (goals := branch.goals)
@[export pantograph_goal_state_root_expr]
protected def GoalState.rootExpr? (goalState: GoalState): Option Expr := do
if goalState.root.name == .anonymous then
.none
let expr ← goalState.mctx.eAssignment.find? goalState.root
let (expr, _) := instantiateMVarsCore (mctx := goalState.mctx) (e := expr)
if expr.hasExprMVar then
-- Must not assert that the goal state is empty here. We could be in a branch goal.
--assert! ¬goalState.goals.isEmpty
.none
else
assert! goalState.goals.isEmpty
return expr
@[export pantograph_goal_state_parent_expr]
protected def GoalState.parentExpr? (goalState: GoalState): Option Expr := do
let parent ← goalState.parentMVar?
let expr := goalState.mctx.eAssignment.find! parent
let (expr, _) := instantiateMVarsCore (mctx := goalState.mctx) (e := expr)
return expr
@[export pantograph_goal_state_get_mvar_e_assignment]
protected def GoalState.getMVarEAssignment (goalState: GoalState) (mvarId: MVarId): Option Expr := do
let expr ← goalState.mctx.eAssignment.find? mvarId
let (expr, _) := instantiateMVarsCore (mctx := goalState.mctx) (e := expr)
return expr
--- Tactic execution functions ---
-- Mimics `Elab.Term.logUnassignedUsingErrorInfos`
private def collectAllErroredMVars (src : MVarId) : Elab.TermElabM (List MVarId) := do
-- These descendants serve as "seed" mvars. If a MVarError's mvar is related
-- to one of these seed mvars, it means an error has occurred when a tactic
-- was executing on `src`. `evalTactic`, will not capture these mvars, so we
-- need to manually find them and save them into the goal list.
let descendants ← Meta.getMVars (.mvar src)
--let _ ← Elab.Term.logUnassignedUsingErrorInfos descendants
let mut alreadyVisited : MVarIdSet := {}
let mut result : MVarIdSet := {}
for { mvarId, .. } in (← get).mvarErrorInfos do
unless alreadyVisited.contains mvarId do
alreadyVisited := alreadyVisited.insert mvarId
/- The metavariable `mvarErrorInfo.mvarId` may have been assigned or
delayed assigned to another metavariable that is unassigned. -/
let mvarDeps ← Meta.getMVars (.mvar mvarId)
if mvarDeps.any descendants.contains then do
result := mvarDeps.foldl (·.insert ·) result
return result.toList
private def mergeMVarLists (li1 li2 : List MVarId) : List MVarId :=
let li2' := li2.filter (¬ li1.contains ·)
li1 ++ li2'
/--
Set `guardMVarErrors` to true to capture mvar errors. Lean will not
automatically collect mvars from text tactics (vide
`test_tactic_failure_synthesize_placeholder`)
-/
protected def GoalState.step (state: GoalState) (goal: MVarId) (tacticM: Elab.Tactic.TacticM Unit) (guardMVarErrors : Bool := false)
: Elab.TermElabM GoalState := do
unless (← getMCtx).decls.contains goal do
throwError s!"Goal is not in context: {goal.name}"
goal.checkNotAssigned `GoalState.step
let (_, { goals }) ← tacticM { elaborator := .anonymous } |>.run { goals := [goal] }
let nextElabState ← MonadBacktrack.saveState
Elab.Term.synthesizeSyntheticMVarsNoPostponing
let goals ← if guardMVarErrors then
pure $ mergeMVarLists goals (← collectAllErroredMVars goal)
else
pure goals
return {
state with
savedState := { term := nextElabState, tactic := { goals }, },
parentMVar? := .some goal,
calcPrevRhs? := .none,
}
/-- Response for executing a tactic -/
inductive TacticResult where
-- Goes to next state
| success (state: GoalState)
-- Tactic failed with messages
| failure (messages: Array String)
-- Could not parse tactic
| parseError (message: String)
-- The given action cannot be executed in the state
| invalidAction (message: String)
private def dumpMessageLog (prevMessageLength : Nat) : CoreM (Array String) := do
let newMessages ← (← Core.getMessageLog).toList.drop prevMessageLength
|>.filterMapM λ m => do
if m.severity == .error then
return .some $ ← m.toString
else
return .none
Core.resetMessageLog
return newMessages.toArray
/-- Executes a `TacticM` monad on this `GoalState`, collecting the errors as necessary -/
protected def GoalState.tryTacticM
(state: GoalState) (goal: MVarId) (tacticM: Elab.Tactic.TacticM Unit)
(guardMVarErrors : Bool := false)
: Elab.TermElabM TacticResult := do
let prevMessageLength := state.coreState.messages.toList.length
try
let nextState ← state.step goal tacticM guardMVarErrors
-- Check if error messages have been generated in the core.
let newMessages ← dumpMessageLog prevMessageLength
if ¬ newMessages.isEmpty then
return .failure newMessages
return .success nextState
catch exception =>
match exception with
| .internal _ => return .failure $ ← dumpMessageLog prevMessageLength
| _ => return .failure #[← exception.toMessageData.toString]
/-- Execute a string tactic on given state. Restores TermElabM -/
@[export pantograph_goal_state_try_tactic_m]
protected def GoalState.tryTactic (state: GoalState) (goal: MVarId) (tactic: String):
Elab.TermElabM TacticResult := do
state.restoreElabM
let tactic ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := if state.isConv then `conv else `tactic)
(input := tactic)
(fileName := ← getFileName) with
| .ok stx => pure $ stx
| .error error => return .parseError error
state.tryTacticM goal (Elab.Tactic.evalTactic tactic) true
protected def GoalState.tryAssign (state: GoalState) (goal: MVarId) (expr: String):
Elab.TermElabM TacticResult := do
state.restoreElabM
let expr ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := expr)
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => return .parseError error
state.tryTacticM goal $ Tactic.evalAssign expr
-- Specialized Tactics
protected def GoalState.tryLet (state: GoalState) (goal: MVarId) (binderName: String) (type: String):
Elab.TermElabM TacticResult := do
state.restoreElabM
let type ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := type)
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => return .parseError error
state.tryTacticM goal $ Tactic.evalLet binderName.toName type
/-- Enter conv tactic mode -/
protected def GoalState.conv (state: GoalState) (goal: MVarId):
Elab.TermElabM TacticResult := do
if state.convMVar?.isSome then
return .invalidAction "Already in conv state"
goal.checkNotAssigned `GoalState.conv
let tacticM : Elab.Tactic.TacticM (Elab.Tactic.SavedState × MVarId) := do
state.restoreTacticM goal
-- See Lean.Elab.Tactic.Conv.convTarget
let convMVar ← Elab.Tactic.withMainContext do
let (rhs, newGoal) ← Elab.Tactic.Conv.mkConvGoalFor (← Elab.Tactic.getMainTarget)
Elab.Tactic.replaceMainGoal [newGoal.mvarId!]
pure rhs.mvarId!
return (← MonadBacktrack.saveState, convMVar)
try
let (nextSavedState, convRhs) ← tacticM { elaborator := .anonymous } |>.run' state.savedState.tactic
-- Other goals are now dormant
let otherGoals := state.goals.filter $ λ g => g != goal
return .success {
root := state.root,
savedState := nextSavedState
parentMVar? := .some goal,
convMVar? := .some (convRhs, goal, otherGoals),
calcPrevRhs? := .none
}
catch exception =>
return .failure #[← exception.toMessageData.toString]
/-- Exit from `conv` mode. Resumes all goals before the mode starts and applys the conv -/
@[export pantograph_goal_state_conv_exit_m]
protected def GoalState.convExit (state: GoalState):
Elab.TermElabM TacticResult := do
let (convRhs, convGoal, _) ← match state.convMVar? with
| .some mvar => pure mvar
| .none => return .invalidAction "Not in conv state"
let tacticM : Elab.Tactic.TacticM Elab.Tactic.SavedState:= do
-- Vide `Lean.Elab.Tactic.Conv.convert`
state.savedState.restore
-- Close all existing goals with `refl`
for mvarId in (← Elab.Tactic.getGoals) do
liftM <| mvarId.refl <|> mvarId.inferInstance <|> pure ()
Elab.Tactic.pruneSolvedGoals
unless (← Elab.Tactic.getGoals).isEmpty do
throwError "convert tactic failed, there are unsolved goals\n{Elab.goalsToMessageData (← Elab.Tactic.getGoals)}"
Elab.Tactic.setGoals [convGoal]
let targetNew ← instantiateMVars (.mvar convRhs)
let proof ← instantiateMVars (.mvar convGoal)
Elab.Tactic.liftMetaTactic1 fun mvarId => mvarId.replaceTargetEq targetNew proof
MonadBacktrack.saveState
try
let nextSavedState ← tacticM { elaborator := .anonymous } |>.run' state.savedState.tactic
return .success {
root := state.root,
savedState := nextSavedState
parentMVar? := .some convGoal,
convMVar? := .none
calcPrevRhs? := .none
}
catch exception =>
return .failure #[← exception.toMessageData.toString]
protected def GoalState.calcPrevRhsOf? (state: GoalState) (goal: MVarId): Option Expr := do
let (mvarId, rhs) ← state.calcPrevRhs?
if mvarId == goal then
.some rhs
else
.none
@[export pantograph_goal_state_try_calc_m]
protected def GoalState.tryCalc (state: GoalState) (goal: MVarId) (pred: String):
Elab.TermElabM TacticResult := do
state.restoreElabM
if state.convMVar?.isSome then
return .invalidAction "Cannot initiate `calc` while in `conv` state"
let `(term|$pred) ← match Parser.runParserCategory
(env := state.env)
(catName := `term)
(input := pred)
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => return .parseError error
goal.checkNotAssigned `GoalState.tryCalc
let calcPrevRhs? := state.calcPrevRhsOf? goal
let decl ← goal.getDecl
let target ← instantiateMVars decl.type
let tag := decl.userName
try
goal.withContext do
let mut step ← Elab.Term.elabType <| ← do
if let some prevRhs := calcPrevRhs? then
Elab.Term.annotateFirstHoleWithType pred (← Meta.inferType prevRhs)
else
pure pred
let some (_, lhs, rhs) ← Elab.Term.getCalcRelation? step |
throwErrorAt pred "invalid 'calc' step, relation expected{indentExpr step}"
if let some prevRhs := calcPrevRhs? then
unless ← Meta.isDefEqGuarded lhs prevRhs do
throwErrorAt pred "invalid 'calc' step, left-hand-side is{indentD m!"{lhs} : {← Meta.inferType lhs}"}\nprevious right-hand-side is{indentD m!"{prevRhs} : {← Meta.inferType prevRhs}"}"
-- Creates a mvar to represent the proof that the calc tactic solves the
-- current branch
-- In the Lean `calc` tactic this is gobbled up by
-- `withCollectingNewGoalsFrom`
let mut proof ← Meta.mkFreshExprMVarAt (← getLCtx) (← Meta.getLocalInstances) step
(userName := tag ++ `calc)
let mvarBranch := proof.mvarId!
let mut proofType ← Meta.inferType proof
let mut remainder? := Option.none
-- The calc tactic either solves the main goal or leaves another relation.
-- Replace the main goal, and save the new goal if necessary
unless ← Meta.isDefEq proofType target do
let rec throwFailed :=
throwError "'calc' tactic failed, has type{indentExpr proofType}\nbut it is expected to have type{indentExpr target}"
let some (_, _, rhs) ← Elab.Term.getCalcRelation? proofType | throwFailed
let some (r, _, rhs') ← Elab.Term.getCalcRelation? target | throwFailed
let lastStep := mkApp2 r rhs rhs'
let lastStepGoal ← Meta.mkFreshExprSyntheticOpaqueMVar lastStep tag
(proof, proofType) ← Elab.Term.mkCalcTrans proof proofType lastStepGoal lastStep
unless ← Meta.isDefEq proofType target do throwFailed
remainder? := .some lastStepGoal.mvarId!
goal.assign proof
let goals := [ mvarBranch ] ++ remainder?.toList
let calcPrevRhs? := remainder?.map $ λ g => (g, rhs)
return .success {
root := state.root,
savedState := {
term := ← MonadBacktrack.saveState,
tactic := { goals },
},
parentMVar? := .some goal,
calcPrevRhs?
}
catch exception =>
return .failure #[← exception.toMessageData.toString]
end Pantograph

231
Pantograph/Library.lean Normal file
View File

@ -0,0 +1,231 @@
import Pantograph.Environment
import Pantograph.Goal
import Pantograph.Protocol
import Pantograph.Delate
import Pantograph.Version
import Lean
namespace Lean
/-- This is better than the default version since it handles `.` and doesn't
crash the program when it fails. -/
def setOptionFromString' (opts : Options) (entry : String) : ExceptT String IO Options := do
let ps := (entry.splitOn "=").map String.trim
let [key, val] ← pure ps | throw "invalid configuration option entry, it must be of the form '<key> = <value>'"
let key := key.toName
let defValue ← getOptionDefaultValue key
match defValue with
| DataValue.ofString _ => pure $ opts.setString key val
| DataValue.ofBool _ =>
match val with
| "true" => pure $ opts.setBool key true
| "false" => pure $ opts.setBool key false
| _ => throw s!"invalid Bool option value '{val}'"
| DataValue.ofName _ => pure $ opts.setName key val.toName
| DataValue.ofNat _ =>
match val.toNat? with
| none => throw s!"invalid Nat option value '{val}'"
| some v => pure $ opts.setNat key v
| DataValue.ofInt _ =>
match val.toInt? with
| none => throw s!"invalid Int option value '{val}'"
| some v => pure $ opts.setInt key v
| DataValue.ofSyntax _ => throw s!"invalid Syntax option value"
end Lean
open Lean
namespace Pantograph
def runMetaM { α } (metaM: MetaM α): CoreM α :=
metaM.run'
def runTermElabM { α } (termElabM: Elab.TermElabM α): CoreM α :=
termElabM.run' (ctx := defaultElabContext) |>.run'
def errorI (type desc: String): Protocol.InteractionError := { error := type, desc := desc }
/-- Adds the given paths to Lean package search path -/
@[export pantograph_init_search]
unsafe def initSearch (sp: String): IO Unit := do
Lean.enableInitializersExecution
Lean.initSearchPath (← Lean.findSysroot) (sp := System.SearchPath.parse sp)
/-- Creates a Core.Context object needed to run all monads -/
@[export pantograph_create_core_context]
def createCoreContext (options: Array String): IO Core.Context := do
let options? ← options.foldlM setOptionFromString' Options.empty |>.run
let options ← match options? with
| .ok options => pure options
| .error e => throw $ IO.userError s!"Options cannot be parsed: {e}"
return {
currNamespace := Name.str .anonymous "Aniva"
openDecls := [], -- No 'open' directives needed
fileName := "<Pantograph>",
fileMap := { source := "", positions := #[0] },
options := options
}
/-- Creates a Core.State object needed to run all monads -/
@[export pantograph_create_core_state]
def createCoreState (imports: Array String): IO Core.State := do
let env ← Lean.importModules
(imports := imports.map (λ str => { module := str.toName, runtimeOnly := false }))
(opts := {})
(trustLevel := 1)
return { env := env }
@[export pantograph_env_add_m]
def envAdd (name: String) (type: String) (value: String) (isTheorem: Bool):
CoreM (Protocol.CR Protocol.EnvAddResult) :=
Environment.addDecl { name, type, value, isTheorem }
@[export pantograph_parse_elab_type_m]
def parseElabType (type: String): Elab.TermElabM (Protocol.CR Expr) := do
let env ← MonadEnv.getEnv
let syn ← match parseTerm env type with
| .error str => return .error $ errorI "parsing" str
| .ok syn => pure syn
match ← elabType syn with
| .error str => return .error $ errorI "elab" str
| .ok expr => return .ok (← instantiateMVars expr)
/-- This must be a TermElabM since the parsed expr contains extra information -/
@[export pantograph_parse_elab_expr_m]
def parseElabExpr (expr: String) (expectedType?: Option String := .none): Elab.TermElabM (Protocol.CR Expr) := do
let env ← MonadEnv.getEnv
let expectedType? ← match ← expectedType?.mapM parseElabType with
| .none => pure $ .none
| .some (.ok t) => pure $ .some t
| .some (.error e) => return .error e
let syn ← match parseTerm env expr with
| .error str => return .error $ errorI "parsing" str
| .ok syn => pure syn
match ← elabTerm syn expectedType? with
| .error str => return .error $ errorI "elab" str
| .ok expr => return .ok (← instantiateMVars expr)
@[export pantograph_expr_echo_m]
def exprEcho (expr: String) (expectedType?: Option String := .none) (levels: Array String := #[]) (options: @&Protocol.Options := {}):
CoreM (Protocol.CR Protocol.ExprEchoResult) :=
runTermElabM $ Elab.Term.withLevelNames (levels.toList.map (·.toName)) do
let expr ← match ← parseElabExpr expr expectedType? with
| .error e => return .error e
| .ok expr => pure expr
try
let type ← unfoldAuxLemmas (← Meta.inferType expr)
return .ok {
type := (← serializeExpression options type),
expr := (← serializeExpression options expr)
}
catch exception =>
return .error $ errorI "typing" (← exception.toMessageData.toString)
@[export pantograph_goal_start_expr_m]
def goalStartExpr (expr: String) (levels: Array String): CoreM (Protocol.CR GoalState) :=
runTermElabM $ Elab.Term.withLevelNames (levels.toList.map (·.toName)) do
let expr ← match ← parseElabType expr with
| .error e => return .error e
| .ok expr => pure $ expr
return .ok $ ← GoalState.create expr
@[export pantograph_goal_resume]
def goalResume (target: GoalState) (goals: Array String): Except String GoalState :=
target.resume (goals.map (λ n => { name := n.toName }) |>.toList)
@[export pantograph_goal_serialize_m]
def goalSerialize (state: GoalState) (options: @&Protocol.Options): CoreM (Array Protocol.Goal) :=
runMetaM <| state.serializeGoals (parent := .none) options
@[export pantograph_goal_print_m]
def goalPrint (state: GoalState) (rootExpr: Bool) (parentExpr: Bool) (goals: Bool) (extraMVars : Array String) (options: @&Protocol.Options)
: CoreM Protocol.GoalPrintResult := runMetaM do
state.restoreMetaM
let root? ← if rootExpr then
state.rootExpr?.mapM λ expr => state.withRootContext do
serializeExpression options (← instantiateAll expr)
else
pure .none
let parent? ← if parentExpr then
state.parentExpr?.mapM λ expr => state.withParentContext do
serializeExpression options (← instantiateAll expr)
else
pure .none
let goals ← if goals then
goalSerialize state options
else
pure #[]
let extraMVars ← extraMVars.mapM λ mvarId => do
let mvarId: MVarId := { name := mvarId.toName }
let .some _ ← mvarId.findDecl? | return {}
state.withContext mvarId do
let .some expr ← getExprMVarAssignment? mvarId | return {}
serializeExpression options (← instantiateAll expr)
return {
root?,
parent?,
goals,
extraMVars,
}
@[export pantograph_goal_tactic_m]
def goalTactic (state: GoalState) (goal: MVarId) (tactic: String): CoreM TacticResult :=
runTermElabM <| state.tryTactic goal tactic
@[export pantograph_goal_assign_m]
def goalAssign (state: GoalState) (goal: MVarId) (expr: String): CoreM TacticResult :=
runTermElabM <| state.tryAssign goal expr
@[export pantograph_goal_have_m]
protected def GoalState.tryHave (state: GoalState) (goal: MVarId) (binderName: String) (type: String): CoreM TacticResult := do
let type ← match (← parseTermM type) with
| .ok syn => pure syn
| .error error => return .parseError error
runTermElabM do
state.restoreElabM
state.tryTacticM goal $ Tactic.evalHave binderName.toName type
@[export pantograph_goal_try_define_m]
protected def GoalState.tryDefine (state: GoalState) (goal: MVarId) (binderName: String) (expr: String): CoreM TacticResult := do
let expr ← match (← parseTermM expr) with
| .ok syn => pure syn
| .error error => return .parseError error
runTermElabM do
state.restoreElabM
state.tryTacticM goal (Tactic.evalDefine binderName.toName expr)
@[export pantograph_goal_try_motivated_apply_m]
protected def GoalState.tryMotivatedApply (state: GoalState) (goal: MVarId) (recursor: String):
Elab.TermElabM TacticResult := do
state.restoreElabM
let recursor ← match (← parseTermM recursor) with
| .ok syn => pure syn
| .error error => return .parseError error
state.tryTacticM goal (tacticM := Tactic.evalMotivatedApply recursor)
@[export pantograph_goal_try_no_confuse_m]
protected def GoalState.tryNoConfuse (state: GoalState) (goal: MVarId) (eq: String):
Elab.TermElabM TacticResult := do
state.restoreElabM
let eq ← match (← parseTermM eq) with
| .ok syn => pure syn
| .error error => return .parseError error
state.tryTacticM goal (tacticM := Tactic.evalNoConfuse eq)
@[export pantograph_goal_try_draft_m]
protected def GoalState.tryDraft (state: GoalState) (goal: MVarId) (expr: String): CoreM TacticResult := do
let expr ← match (← parseTermM expr) with
| .ok syn => pure syn
| .error error => return .parseError error
runTermElabM do
state.restoreElabM
state.tryTacticM goal (Tactic.evalDraft expr)
@[export pantograph_goal_let_m]
def goalLet (state: GoalState) (goal: MVarId) (binderName: String) (type: String): CoreM TacticResult :=
runTermElabM <| state.tryLet goal binderName type
@[export pantograph_goal_conv_m]
def goalConv (state: GoalState) (goal: MVarId): CoreM TacticResult :=
runTermElabM <| state.conv goal
@[export pantograph_goal_conv_exit_m]
def goalConvExit (state: GoalState): CoreM TacticResult :=
runTermElabM <| state.convExit
@[export pantograph_goal_calc_m]
def goalCalc (state: GoalState) (goal: MVarId) (pred: String): CoreM TacticResult :=
runTermElabM <| state.tryCalc goal pred
end Pantograph

369
Pantograph/Protocol.lean Normal file
View File

@ -0,0 +1,369 @@
/-
All the command input/output structures are stored here
Note that no command other than `InteractionError` may have `error` as one of
its field names to avoid confusion with error messages generated by the REPL.
-/
import Lean.Data.Json
import Lean.Data.Position
namespace Pantograph.Protocol
/-- Main Option structure, placed here to avoid name collision -/
structure Options where
-- When false, suppress newlines in Json objects. Useful for machine-to-machine interaction.
-- This should be false` by default to avoid any surprises with parsing.
printJsonPretty: Bool := false
-- When enabled, pretty print every expression
printExprPretty: Bool := true
-- When enabled, print the raw AST of expressions
printExprAST: Bool := false
printDependentMVars: Bool := false
-- When enabled, the types and values of persistent variables in a goal
-- are not shown unless they are new to the proof step. Reduces overhead.
-- NOTE: that this assumes the type and assignment of variables can never change.
noRepeat: Bool := false
-- See `pp.auxDecls`
printAuxDecls: Bool := false
-- See `pp.implementationDetailHyps`
printImplementationDetailHyps: Bool := false
-- If this is set to `true`, goals will never go dormant, so you don't have to manage resumption
automaticMode: Bool := true
deriving Lean.ToJson
abbrev OptionsT := ReaderT Options
--- Expression Objects ---
structure BoundExpression where
binders: Array (String × String)
target: String
deriving Lean.ToJson
structure Expression where
-- Pretty printed expression
pp?: Option String := .none
-- AST structure
sexp?: Option String := .none
dependentMVars?: Option (Array String) := .none
deriving Lean.ToJson
structure Variable where
/-- The internal name used in raw expressions -/
name: String := ""
/-- The name displayed to the user -/
userName: String
/-- Does the name contain a dagger -/
isInaccessible: Bool := false
type?: Option Expression := .none
value?: Option Expression := .none
deriving Lean.ToJson
structure Goal where
name: String := ""
/-- Name of the metavariable -/
userName?: Option String := .none
/-- Is the goal in conversion mode -/
isConversion: Bool := false
/-- target expression type -/
target: Expression
/-- Variables -/
vars: Array Variable := #[]
deriving Lean.ToJson
--- Individual Commands and return types ---
structure Command where
cmd: String
payload: Lean.Json
deriving Lean.FromJson
structure InteractionError where
error: String
desc: String
deriving Lean.ToJson
def errorIndex (desc: String): InteractionError := { error := "index", desc }
def errorExpr (desc: String): InteractionError := { error := "expr", desc }
--- Individual command and return types ---
structure Reset where
deriving Lean.FromJson
structure Stat where
deriving Lean.FromJson
structure StatResult where
-- Number of goals states
nGoals: Nat
deriving Lean.ToJson
-- Return the type of an expression
structure ExprEcho where
expr: String
type?: Option String
-- universe levels
levels: Option (Array String) := .none
deriving Lean.FromJson
structure ExprEchoResult where
expr: Expression
type: Expression
deriving Lean.ToJson
-- Print all symbols in environment
structure EnvCatalog where
deriving Lean.FromJson
structure EnvCatalogResult where
symbols: Array String
deriving Lean.ToJson
-- Print the type of a symbol
structure EnvInspect where
name: String
-- Show the value expressions; By default definitions values are shown and
-- theorem values are hidden.
value?: Option Bool := .some false
-- Show the type and value dependencies
dependency?: Option Bool := .some false
-- Show source location
source?: Option Bool := .some false
deriving Lean.FromJson
-- See `InductiveVal`
structure InductInfo where
numParams: Nat
numIndices: Nat
all: Array String
ctors: Array String
isRec: Bool := false
isReflexive: Bool := false
isNested: Bool := false
deriving Lean.ToJson
-- See `ConstructorVal`
structure ConstructorInfo where
induct: String
cidx: Nat
numParams: Nat
numFields: Nat
deriving Lean.ToJson
/-- See `Lean/Declaration.lean` -/
structure RecursorRule where
ctor: String
nFields: Nat
rhs: Expression
deriving Lean.ToJson
structure RecursorInfo where
all: Array String
numParams: Nat
numIndices: Nat
numMotives: Nat
numMinors: Nat
rules: Array RecursorRule
k: Bool
deriving Lean.ToJson
structure EnvInspectResult where
type: Expression
isUnsafe: Bool := false
value?: Option Expression := .none
module?: Option String := .none
-- If the name is private, displays the public facing name
publicName?: Option String := .none
typeDependency?: Option (Array String) := .none
valueDependency?: Option (Array String) := .none
inductInfo?: Option InductInfo := .none
constructorInfo?: Option ConstructorInfo := .none
recursorInfo?: Option RecursorInfo := .none
-- Location in source
sourceUri?: Option String := .none
sourceStart?: Option Lean.Position := .none
sourceEnd?: Option Lean.Position := .none
deriving Lean.ToJson
structure EnvAdd where
name: String
type: String
value: String
isTheorem: Bool
deriving Lean.FromJson
structure EnvAddResult where
deriving Lean.ToJson
structure EnvSaveLoad where
path: System.FilePath
deriving Lean.FromJson
structure EnvSaveLoadResult where
deriving Lean.ToJson
/-- Set options; See `Options` struct above for meanings -/
structure OptionsSet where
printJsonPretty?: Option Bool
printExprPretty?: Option Bool
printExprAST?: Option Bool
printDependentMVars?: Option Bool
noRepeat?: Option Bool
printAuxDecls?: Option Bool
printImplementationDetailHyps?: Option Bool
automaticMode?: Option Bool
deriving Lean.FromJson
structure OptionsSetResult where
deriving Lean.ToJson
structure OptionsPrint where
deriving Lean.FromJson
structure GoalStart where
-- Only one of the fields below may be populated.
expr: Option String -- Directly parse in an expression
-- universe levels
levels: Option (Array String) := .none
copyFrom: Option String -- Copy the type from a theorem in the environment
deriving Lean.FromJson
structure GoalStartResult where
stateId: Nat := 0
-- Name of the root metavariable
root: String
deriving Lean.ToJson
structure GoalTactic where
-- Identifiers for tree, state, and goal
stateId: Nat
goalId: Nat := 0
-- One of the fields here must be filled
tactic?: Option String := .none
expr?: Option String := .none
have?: Option String := .none
let?: Option String := .none
calc?: Option String := .none
-- true to enter `conv`, `false` to exit. In case of exit the `goalId` is ignored.
conv?: Option Bool := .none
draft?: Option String := .none
-- In case of the `have` tactic, the new free variable name is provided here
binderName?: Option String := .none
deriving Lean.FromJson
structure GoalTacticResult where
-- The next goal state id. Existence of this field shows success
nextStateId?: Option Nat := .none
-- If the array is empty, it shows the goals have been fully resolved.
goals?: Option (Array Goal) := .none
-- Existence of this field shows tactic execution failure
tacticErrors?: Option (Array String) := .none
-- Existence of this field shows the tactic parsing has failed
parseError?: Option String := .none
deriving Lean.ToJson
structure GoalContinue where
-- State from which the continuation acquires the context
target: Nat
-- One of the following must be supplied
-- The state which is an ancestor of `target` where goals will be extracted from
branch?: Option Nat := .none
-- Or, the particular goals that should be brought back into scope
goals?: Option (Array String) := .none
deriving Lean.FromJson
structure GoalContinueResult where
nextStateId: Nat
goals: (Array Goal)
deriving Lean.ToJson
-- Remove goal states
structure GoalDelete where
-- This is ok being a List because it doesn't show up in the ABI
stateIds: List Nat
deriving Lean.FromJson
structure GoalDeleteResult where
deriving Lean.ToJson
structure GoalPrint where
stateId: Nat
-- Print root?
rootExpr?: Option Bool := .some False
-- Print the parent expr?
parentExpr?: Option Bool := .some False
-- Print goals?
goals?: Option Bool := .some False
-- Print values of extra mvars?
extraMVars?: Option (Array String) := .none
deriving Lean.FromJson
structure GoalPrintResult where
-- The root expression
root?: Option Expression := .none
-- The filling expression of the parent goal
parent?: Option Expression := .none
goals: Array Goal := #[]
extraMVars: Array Expression := #[]
deriving Lean.ToJson
-- Diagnostic Options, not available in REPL
structure GoalDiag where
printContext: Bool := true
printValue: Bool := true
printNewMVars: Bool := false
-- Print all mvars
printAll: Bool := false
instantiate: Bool := true
printSexp: Bool := false
structure GoalSave where
id: Nat
path: System.FilePath
deriving Lean.FromJson
structure GoalSaveResult where
deriving Lean.ToJson
structure GoalLoad where
path: System.FilePath
deriving Lean.FromJson
structure GoalLoadResult where
id: Nat
deriving Lean.ToJson
/-- Executes the Lean compiler on a single file -/
structure FrontendProcess where
-- One of these two must be supplied: Either supply the file name or the content.
fileName?: Option String := .none
file?: Option String := .none
-- collect tactic invocations
invocations: Bool := false
-- collect `sorry`s
sorrys: Bool := false
-- collect type errors
typeErrorsAsGoals: Bool := false
-- list new constants from each compilation step
newConstants: Bool := false
deriving Lean.FromJson
structure InvokedTactic where
goalBefore: String
goalAfter: String
tactic: String
-- List of used constants
usedConstants: Array String
deriving Lean.ToJson
structure CompilationUnit where
-- String boundaries of compilation units
boundary: (Nat × Nat)
messages: Array String := #[]
-- Tactic invocations
invocations?: Option (List InvokedTactic) := .none
goalStateId?: Option Nat := .none
goals?: Option (Array Goal) := .none
-- Code segments which generated the goals
goalSrcBoundaries?: Option (Array (Nat × Nat)) := .none
-- New constants defined in compilation unit
newConstants?: Option (Array String) := .none
deriving Lean.ToJson
structure FrontendProcessResult where
units: List CompilationUnit
deriving Lean.ToJson
abbrev CR α := Except InteractionError α
end Pantograph.Protocol

162
Pantograph/Serial.lean Normal file
View File

@ -0,0 +1,162 @@
import Lean.Environment
import Lean.Replay
import Init.System.IOError
import Std.Data.HashMap
import Pantograph.Goal
/-!
Input/Output functions
# Pickling and unpickling objects
By abusing `saveModuleData` and `readModuleData` we can pickle and unpickle objects to disk.
-/
open Lean
namespace Pantograph
/--
Save an object to disk.
If you need to write multiple objects from within a single declaration,
you will need to provide a unique `key` for each.
-/
def pickle {α : Type} (path : System.FilePath) (x : α) (key : Name := by exact decl_name%) : IO Unit :=
saveModuleData path key (unsafe unsafeCast x)
/--
Load an object from disk.
Note: The returned `CompactedRegion` can be used to free the memory behind the value
of type `α`, using `CompactedRegion.free` (which is only safe once all references to the `α` are
released). Ignoring the `CompactedRegion` results in the data being leaked.
Use `withUnpickle` to call `CompactedRegion.free` automatically.
This function is unsafe because the data being loaded may not actually have type `α`, and this
may cause crashes or other bad behavior.
-/
unsafe def unpickle (α : Type) (path : System.FilePath) : IO (α × CompactedRegion) := do
let (x, region) ← readModuleData path
pure (unsafeCast x, region)
/-- Load an object from disk and run some continuation on it, freeing memory afterwards. -/
unsafe def withUnpickle [Monad m] [MonadLiftT IO m] {α β : Type}
(path : System.FilePath) (f : α → m β) : m β := do
let (x, region) ← unpickle α path
let r ← f x
region.free
pure r
/--
Pickle an `Environment` to disk.
We only store:
* the list of imports
* the new constants from `Environment.constants`
and when unpickling, we build a fresh `Environment` from the imports,
and then add the new constants.
-/
@[export pantograph_env_pickle_m]
def environmentPickle (env : Environment) (path : System.FilePath) : IO Unit :=
Pantograph.pickle path (env.header.imports, env.constants.map₂)
/--
Unpickle an `Environment` from disk.
We construct a fresh `Environment` with the relevant imports,
and then replace the new constants.
-/
@[export pantograph_env_unpickle_m]
def environmentUnpickle (path : System.FilePath) : IO (Environment × CompactedRegion) := unsafe do
let ((imports, map₂), region) ← Pantograph.unpickle (Array Import × PHashMap Name ConstantInfo) path
let env ← importModules imports {} 0
return (← env.replay (Std.HashMap.ofList map₂.toList), region)
open Lean.Core in
structure CompactCoreState where
-- env : Environment
nextMacroScope : MacroScope := firstFrontendMacroScope + 1
ngen : NameGenerator := {}
-- traceState : TraceState := {}
-- cache : Cache := {}
-- messages : MessageLog := {}
-- infoState : Elab.InfoState := {}
@[export pantograph_goal_state_pickle_m]
def goalStatePickle (goalState : GoalState) (path : System.FilePath) : IO Unit :=
let {
savedState := {
term := {
meta := {
core,
meta,
}
«elab»,
},
tactic
}
root,
parentMVar?,
convMVar?,
calcPrevRhs?,
} := goalState
--let env := core.env
Pantograph.pickle path (
({ core with } : CompactCoreState),
meta,
«elab»,
tactic,
root,
parentMVar?,
convMVar?,
calcPrevRhs?,
)
@[export pantograph_goal_state_unpickle_m]
def goalStateUnpickle (path : System.FilePath) (env : Environment)
: IO (GoalState × CompactedRegion) := unsafe do
let ((
compactCore,
meta,
«elab»,
tactic,
root,
parentMVar?,
convMVar?,
calcPrevRhs?,
), region) ← Pantograph.unpickle (
CompactCoreState ×
Meta.State ×
Elab.Term.State ×
Elab.Tactic.State ×
MVarId ×
Option MVarId ×
Option (MVarId × MVarId × List MVarId) ×
Option (MVarId × Expr)
) path
let goalState := {
savedState := {
term := {
meta := {
core := {
compactCore with
passedHeartbeats := 0,
env,
},
meta,
},
«elab»,
},
tactic,
},
root,
parentMVar?,
convMVar?,
calcPrevRhs?,
}
return (goalState, region)
end Pantograph

5
Pantograph/Tactic.lean Normal file
View File

@ -0,0 +1,5 @@
import Pantograph.Tactic.Assign
import Pantograph.Tactic.Congruence
import Pantograph.Tactic.MotivatedApply
import Pantograph.Tactic.NoConfuse
import Pantograph.Tactic.Prograde

View File

@ -0,0 +1,64 @@
import Lean
open Lean
namespace Pantograph.Tactic
/-- WARNING: This should be used with a function like `elabTermWithHoles` that properly collects the mvar information from `expr`. -/
def assign (goal: MVarId) (expr: Expr) (nextGoals: List MVarId): MetaM (List MVarId) := do
goal.checkNotAssigned `Pantograph.Tactic.assign
-- This run of the unifier is critical in resolving mvars in passing
let exprType ← Meta.inferType expr
let goalType ← goal.getType
unless ← Meta.isDefEq goalType exprType do
throwError s!"{← Meta.ppExpr expr} : {← Meta.ppExpr exprType} ≠ {← Meta.ppExpr goalType}"
goal.assign expr
nextGoals.filterM (not <$> ·.isAssigned)
def evalAssign : Elab.Tactic.Tactic := fun stx => Elab.Tactic.withMainContext do
let target ← Elab.Tactic.getMainTarget
let goal ← Elab.Tactic.getMainGoal
goal.checkNotAssigned `Pantograph.Tactic.evalAssign
let (expr, nextGoals) ← Elab.Tactic.elabTermWithHoles stx
(expectedType? := .some target)
(tagSuffix := .anonymous )
(allowNaturalHoles := true)
goal.assign expr
Elab.Tactic.replaceMainGoal nextGoals
def sorryToHole (src : Expr) : StateRefT (List MVarId) MetaM Expr := do
Meta.transform src λ
| .app (.app (.const ``sorryAx ..) type) .. => do
let type ← instantiateMVars type
if type.hasSorry then
throwError s!"Coupling is not allowed in draft tactic: {← Meta.ppExpr type}"
let mvar ← Meta.mkFreshExprSyntheticOpaqueMVar type
modify (mvar.mvarId! :: .)
pure $ .done mvar
| _ => pure .continue
-- Given a complete (no holes) expression, extract the sorry's from it and convert them into goals.
def draft (goal : MVarId) (expr : Expr) : MetaM (List MVarId) := do
goal.checkNotAssigned `Pantograph.Tactic.draft
let exprType ← Meta.inferType expr
let goalType ← goal.getType
unless ← Meta.isDefEq goalType exprType do
throwError s!"{← Meta.ppExpr expr} : {← Meta.ppExpr exprType} ≠ {← Meta.ppExpr goalType}"
let (expr', holes) ← sorryToHole expr |>.run []
goal.assign expr'
return holes.reverse
def evalDraft : Elab.Tactic.Tactic := fun stx ↦ Elab.Tactic.withMainContext do
let target ← Elab.Tactic.getMainTarget
let goal ← Elab.Tactic.getMainGoal
let (expr, holeGoals) ← Elab.Tactic.elabTermWithHoles stx
(expectedType? := .some target)
(tagSuffix := .anonymous)
(allowNaturalHoles := true)
let draftGoals ← draft goal expr
Elab.Tactic.replaceMainGoal $ holeGoals ++ draftGoals
end Pantograph.Tactic

View File

@ -0,0 +1,98 @@
import Lean
open Lean
namespace Pantograph.Tactic
def congruenceArg (mvarId: MVarId): MetaM (List MVarId) := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.congruenceArg
let target ← mvarId.getType
let .some (β, _, _) := (← instantiateMVars target).eq? | throwError "Goal is not an Eq"
let userName := (← mvarId.getDecl).userName
let u ← Meta.mkFreshLevelMVar
let α ← Meta.mkFreshExprSyntheticOpaqueMVar (mkSort u)
(tag := userName ++ `α)
let f ← Meta.mkFreshExprSyntheticOpaqueMVar (.forallE .anonymous α β .default)
(tag := userName ++ `f)
let a₁ ← Meta.mkFreshExprSyntheticOpaqueMVar α
(tag := userName ++ `a₁)
let a₂ ← Meta.mkFreshExprSyntheticOpaqueMVar α
(tag := userName ++ `a₂)
let h ← Meta.mkFreshExprSyntheticOpaqueMVar (← Meta.mkEq a₁ a₂)
(tag := userName ++ `h)
let conduitType ← Meta.mkEq (← Meta.mkEq (.app f a₁) (.app f a₂)) target
let conduit ← Meta.mkFreshExprSyntheticOpaqueMVar conduitType
(tag := userName ++ `conduit)
mvarId.assign $ ← Meta.mkEqMP conduit (← Meta.mkCongrArg f h)
let result := [α, a₁, a₂, f, h, conduit]
return result.map (·.mvarId!)
def evalCongruenceArg: Elab.Tactic.TacticM Unit := do
let goal ← Elab.Tactic.getMainGoal
let nextGoals ← congruenceArg goal
Elab.Tactic.replaceMainGoal nextGoals
def congruenceFun (mvarId: MVarId): MetaM (List MVarId) := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.congruenceFun
let target ← mvarId.getType
let .some (β, _, _) := (← instantiateMVars target).eq? | throwError "Goal is not an Eq"
let userName := (← mvarId.getDecl).userName
let u ← Meta.mkFreshLevelMVar
let α ← Meta.mkFreshExprSyntheticOpaqueMVar (mkSort u)
(tag := userName ++ `α)
let fType := .forallE .anonymous α β .default
let f₁ ← Meta.mkFreshExprSyntheticOpaqueMVar fType
(tag := userName ++ `f₁)
let f₂ ← Meta.mkFreshExprSyntheticOpaqueMVar fType
(tag := userName ++ `f₂)
let a ← Meta.mkFreshExprSyntheticOpaqueMVar α
(tag := userName ++ `a)
let h ← Meta.mkFreshExprSyntheticOpaqueMVar (← Meta.mkEq f₁ f₂)
(tag := userName ++ `h)
let conduitType ← Meta.mkEq (← Meta.mkEq (.app f₁ a) (.app f₂ a)) target
let conduit ← Meta.mkFreshExprSyntheticOpaqueMVar conduitType
(tag := userName ++ `conduit)
mvarId.assign $ ← Meta.mkEqMP conduit (← Meta.mkCongrFun h a)
let result := [α, f₁, f₂, h, a, conduit]
return result.map (·.mvarId!)
def evalCongruenceFun: Elab.Tactic.TacticM Unit := do
let goal ← Elab.Tactic.getMainGoal
let nextGoals ← congruenceFun goal
Elab.Tactic.replaceMainGoal nextGoals
def congruence (mvarId: MVarId): MetaM (List MVarId) := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.congruence
let target ← mvarId.getType
let .some (β, _, _) := (← instantiateMVars target).eq? | throwError "Goal is not an Eq"
let userName := (← mvarId.getDecl).userName
let u ← Meta.mkFreshLevelMVar
let α ← Meta.mkFreshExprSyntheticOpaqueMVar (mkSort u)
(tag := userName ++ `α)
let fType := .forallE .anonymous α β .default
let f₁ ← Meta.mkFreshExprSyntheticOpaqueMVar fType
(tag := userName ++ `f₁)
let f₂ ← Meta.mkFreshExprSyntheticOpaqueMVar fType
(tag := userName ++ `f₂)
let a₁ ← Meta.mkFreshExprSyntheticOpaqueMVar α
(tag := userName ++ `a₁)
let a₂ ← Meta.mkFreshExprSyntheticOpaqueMVar α
(tag := userName ++ `a₂)
let h₁ ← Meta.mkFreshExprSyntheticOpaqueMVar (← Meta.mkEq f₁ f₂)
(tag := userName ++ `h₁)
let h₂ ← Meta.mkFreshExprSyntheticOpaqueMVar (← Meta.mkEq a₁ a₂)
(tag := userName ++ `h₂)
let conduitType ← Meta.mkEq (← Meta.mkEq (.app f₁ a₁) (.app f₂ a₂)) target
let conduit ← Meta.mkFreshExprSyntheticOpaqueMVar conduitType
(tag := userName ++ `conduit)
mvarId.assign $ ← Meta.mkEqMP conduit (← Meta.mkCongr h₁ h₂)
let result := [α, f₁, f₂, a₁, a₂, h₁, h₂, conduit]
return result.map (·.mvarId!)
def evalCongruence: Elab.Tactic.TacticM Unit := do
let goal ← Elab.Tactic.getMainGoal
let nextGoals ← congruence goal
Elab.Tactic.replaceMainGoal nextGoals
end Pantograph.Tactic

View File

@ -0,0 +1,106 @@
import Lean
open Lean
namespace Pantograph.Tactic
def getForallArgsBody: Expr → List Expr × Expr
| .forallE _ d b _ =>
let (innerArgs, innerBody) := getForallArgsBody b
(d :: innerArgs, innerBody)
| e => ([], e)
def replaceForallBody: Expr → Expr → Expr
| .forallE param domain body binderInfo, target =>
let body := replaceForallBody body target
.forallE param domain body binderInfo
| _, target => target
structure RecursorWithMotive where
args: List Expr
body: Expr
-- .bvar index for the motive and major from the body
iMotive: Nat
namespace RecursorWithMotive
protected def nArgs (info: RecursorWithMotive): Nat := info.args.length
protected def getMotiveType (info: RecursorWithMotive): Expr :=
let level := info.nArgs - info.iMotive - 1
let a := info.args.get! level
a
protected def surrogateMotiveType (info: RecursorWithMotive) (mvars: Array Expr) (resultant: Expr): MetaM Expr := do
let motiveType := Expr.instantiateRev info.getMotiveType mvars
let resultantType ← Meta.inferType resultant
return replaceForallBody motiveType resultantType
protected def conduitType (info: RecursorWithMotive) (mvars: Array Expr) (resultant: Expr): MetaM Expr := do
let motiveCall := Expr.instantiateRev info.body mvars
Meta.mkEq motiveCall resultant
end RecursorWithMotive
def getRecursorInformation (recursorType: Expr): Option RecursorWithMotive := do
let (args, body) := getForallArgsBody recursorType
if ¬ body.isApp then
.none
let iMotive ← match body.getAppFn with
| .bvar iMotive => pure iMotive
| _ => .none
return {
args,
body,
iMotive,
}
def collectMotiveArguments (forallBody: Expr): SSet Nat :=
match forallBody with
| .app (.bvar i) _ => SSet.empty.insert i
| _ => SSet.empty
/-- Applies a symbol of the type `∀ (motive: α → Sort u) (a: α)..., (motive α)` -/
def motivatedApply (mvarId: MVarId) (recursor: Expr) : MetaM (Array Meta.InductionSubgoal) := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.motivatedApply
let recursorType ← Meta.inferType recursor
let resultant ← mvarId.getType
let tag ← mvarId.getTag
let info ← match getRecursorInformation recursorType with
| .some info => pure info
| .none => throwError "Recursor return type does not correspond with the invocation of a motive: {← Meta.ppExpr recursorType}"
let rec go (i: Nat) (prev: Array Expr): MetaM (Array Expr) := do
if i ≥ info.nArgs then
return prev
else
let argType := info.args.get! i
-- If `argType` has motive references, its goal needs to be placed in it
let argType := argType.instantiateRev prev
let bvarIndex := info.nArgs - i - 1
let argGoal ← if bvarIndex = info.iMotive then
let surrogateMotiveType ← info.surrogateMotiveType prev resultant
Meta.mkFreshExprSyntheticOpaqueMVar surrogateMotiveType (tag := tag ++ `motive)
else
Meta.mkFreshExprSyntheticOpaqueMVar argType (tag := .anonymous)
let prev := prev ++ [argGoal]
go (i + 1) prev
termination_by info.nArgs - i
let mut newMVars ← go 0 #[]
-- Create the conduit type which proves the result of the motive is equal to the goal
let conduitType ← info.conduitType newMVars resultant
let goalConduit ← Meta.mkFreshExprSyntheticOpaqueMVar conduitType (tag := `conduit)
mvarId.assign $ ← Meta.mkEqMP goalConduit (mkAppN recursor newMVars)
newMVars := newMVars ++ [goalConduit]
return newMVars.map (λ mvar => { mvarId := mvar.mvarId!})
def evalMotivatedApply : Elab.Tactic.Tactic := fun stx => Elab.Tactic.withMainContext do
let recursor ← Elab.Term.elabTerm (stx := stx) .none
let nextGoals ← motivatedApply (← Elab.Tactic.getMainGoal) recursor
Elab.Tactic.replaceMainGoal $ nextGoals.toList.map (·.mvarId)
end Pantograph.Tactic

View File

@ -0,0 +1,22 @@
import Lean
open Lean
namespace Pantograph.Tactic
def noConfuse (mvarId: MVarId) (h: Expr): MetaM Unit := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.noConfuse
let target ← mvarId.getType
let noConfusion ← Meta.mkNoConfusion (target := target) (h := h)
unless ← Meta.isDefEq (← Meta.inferType noConfusion) target do
throwError "invalid noConfuse call: The resultant type {← Meta.ppExpr $ ← Meta.inferType noConfusion} cannot be unified with {← Meta.ppExpr target}"
mvarId.assign noConfusion
def evalNoConfuse: Elab.Tactic.Tactic := λ stx => do
let goal ← Elab.Tactic.getMainGoal
let h ← goal.withContext $ Elab.Term.elabTerm (stx := stx) .none
noConfuse goal h
Elab.Tactic.replaceMainGoal []
end Pantograph.Tactic

View File

@ -0,0 +1,88 @@
/- Prograde (forward) reasoning tactics -/
import Lean
open Lean
namespace Pantograph.Tactic
private def mkUpstreamMVar (goal: MVarId) : MetaM Expr := do
Meta.mkFreshExprSyntheticOpaqueMVar (← goal.getType) (tag := ← goal.getTag)
/-- Introduces a fvar to the current mvar -/
def define (mvarId: MVarId) (binderName: Name) (expr: Expr): MetaM (FVarId × MVarId) := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.define
let type ← Meta.inferType expr
Meta.withLetDecl binderName type expr λ fvar => do
let mvarUpstream ← mkUpstreamMVar mvarId
mvarId.assign $ ← Meta.mkLetFVars #[fvar] mvarUpstream
pure (fvar.fvarId!, mvarUpstream.mvarId!)
def evalDefine (binderName: Name) (expr: Syntax): Elab.Tactic.TacticM Unit := do
let goal ← Elab.Tactic.getMainGoal
let expr ← goal.withContext $ Elab.Term.elabTerm (stx := expr) (expectedType? := .none)
let (_, mvarId) ← define goal binderName expr
Elab.Tactic.replaceMainGoal [mvarId]
structure BranchResult where
fvarId?: Option FVarId := .none
branch: MVarId
main: MVarId
def «have» (mvarId: MVarId) (binderName: Name) (type: Expr): MetaM BranchResult := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.have
let lctx ← MonadLCtx.getLCtx
-- The branch goal inherits the same context, but with a different type
let mvarBranch ← Meta.mkFreshExprMVarAt lctx (← Meta.getLocalInstances) type
-- Create the context for the `upstream` goal
let fvarId ← mkFreshFVarId
let lctxUpstream := lctx.mkLocalDecl fvarId binderName type
let mvarUpstream ←
Meta.withLCtx lctxUpstream #[] do
Meta.withNewLocalInstances #[.fvar fvarId] 0 do
let mvarUpstream ← mkUpstreamMVar mvarId
--let expr: Expr := .app (.lam binderName type mvarBranch .default) mvarUpstream
mvarId.assign $ ← Meta.mkLambdaFVars #[.fvar fvarId] mvarUpstream
pure mvarUpstream
return {
fvarId? := .some fvarId,
branch := mvarBranch.mvarId!,
main := mvarUpstream.mvarId!,
}
def evalHave (binderName: Name) (type: Syntax): Elab.Tactic.TacticM Unit := do
let goal ← Elab.Tactic.getMainGoal
let nextGoals: List MVarId ← goal.withContext do
let type ← Elab.Term.elabType (stx := type)
let result ← «have» goal binderName type
pure [result.branch, result.main]
Elab.Tactic.replaceMainGoal nextGoals
def «let» (mvarId: MVarId) (binderName: Name) (type: Expr): MetaM BranchResult := mvarId.withContext do
mvarId.checkNotAssigned `Pantograph.Tactic.let
let lctx ← MonadLCtx.getLCtx
-- The branch goal inherits the same context, but with a different type
let mvarBranch ← Meta.mkFreshExprMVarAt lctx (← Meta.getLocalInstances) type (userName := binderName)
assert! ¬ type.hasLooseBVars
let mvarUpstream ← Meta.withLetDecl binderName type mvarBranch $ λ fvar => do
let mvarUpstream ← mkUpstreamMVar mvarId
mvarId.assign $ ← Meta.mkLetFVars #[fvar] mvarUpstream
pure mvarUpstream
return {
branch := mvarBranch.mvarId!,
main := mvarUpstream.mvarId!,
}
def evalLet (binderName: Name) (type: Syntax): Elab.Tactic.TacticM Unit := do
let goal ← Elab.Tactic.getMainGoal
let type ← goal.withContext $ Elab.Term.elabType (stx := type)
let result ← «let» goal binderName type
Elab.Tactic.replaceMainGoal [result.branch, result.main]
end Pantograph.Tactic

6
Pantograph/Version.lean Normal file
View File

@ -0,0 +1,6 @@
namespace Pantograph
@[export pantograph_version]
def version := "0.2.25"
end Pantograph

166
README.md
View File

@ -1,60 +1,130 @@
# PyPantograph
# Pantograph
A Machine-to-Machine Interaction System for Lean 4.
A Machine-to-Machine interaction system for Lean 4.
![Pantograph](doc/icon.svg)
Pantograph provides interfaces to execute proofs, construct expressions, and
examine the symbol list of a Lean project for machine learning.
See [documentations](doc/rationale.md) for design rationale and references.
## Installation
1. Install `poetry`
2. Clone this repository with submodules:
For Nix users, run
``` sh
nix build .#{sharedLib,executable}
```
to build either the shared library or executable.
Install `lake` and `lean` fixed to the version of the `lean-toolchain` file, and
run
``` sh
lake build
```
This builds the executable in `.lake/build/bin/pantograph-repl`.
## Executable Usage
``` sh
pantograph-repl MODULES|LEAN_OPTIONS
```
The `pantograph-repl` executable must be run with a list of modules to import.
It can also accept lean options of the form `--key=value` e.g. `--pp.raw=true`.
The REPL loop accepts commands as single-line JSON inputs and outputs either an
`Error:` (indicating malformed command) or a JSON return value indicating the
result of a command execution. The command can be passed in one of two formats
```
command { ... }
{ "cmd": command, "payload": ... }
```
The list of available commands can be found in `Pantograph/Protocol.lean` and below. An
empty command aborts the REPL.
Example: (~5k symbols)
```
$ pantograph Init
env.catalog
env.inspect {"name": "Nat.le_add_left"}
```
Example with `mathlib4` (~90k symbols, may stack overflow, see troubleshooting)
```
$ pantograph Mathlib.Analysis.Seminorm
env.catalog
```
Example proving a theorem: (alternatively use `goal.start {"copyFrom": "Nat.add_comm"}`) to prime the proof
```
$ pantograph Init
goal.start {"expr": "∀ (n m : Nat), n + m = m + n"}
goal.tactic {"stateId": 0, "goalId": 0, "tactic": "intro n m"}
goal.tactic {"stateId": 1, "goalId": 0, "tactic": "assumption"}
goal.delete {"stateIds": [0]}
stat {}
goal.tactic {"stateId": 1, "goalId": 0, "tactic": "rw [Nat.add_comm]"}
stat
```
where the application of `assumption` should lead to a failure.
For a list of commands, see [REPL Documentation](doc/repl.md).
### Project Environment
To use Pantograph in a project environment, setup the `LEAN_PATH` environment
variable so it contains the library path of lean libraries. The libraries must
be built in advance. For example, if `mathlib4` is stored at `../lib/mathlib4`,
the environment might be setup like this:
``` sh
LIB="../lib"
LIB_MATHLIB="$LIB/mathlib4/.lake"
export LEAN_PATH="$LIB/mathlib4/build/lib:$LIB_MATHLIB/aesop/build/lib:$LIB_MATHLIB/Qq/build/lib:$LIB_MATHLIB/std/build/lib"
LEAN_PATH=$LEAN_PATH build/bin/pantograph $@
```
The `$LEAN_PATH` executable of any project can be extracted by
``` sh
lake env printenv LEAN_PATH
```
### Troubleshooting
If lean encounters stack overflow problems when printing catalog, execute this before running lean:
```sh
git clone --recurse-submodules <repo-path>
```
3. Install `elan` and `lake`: See [Lean Manual](https://docs.lean-lang.org/lean4/doc/setup.html)
4. Execute
```sh
poetry build
poetry install
ulimit -s unlimited
```
## Documentation
## Library Usage
Build the documentations by
```sh
poetry install --only doc
poetry run jupyter-book build docs
`Pantograph/Library.lean` exposes a series of interfaces which allow FFI call
with `Pantograph` which mirrors the REPL commands above. It is recommended to
call Pantograph via this FFI since it provides a tremendous speed up.
The executable can be used as-is, but linking against the shared library
requires the presence of `lean-all`. Note that there isn't a 1-1 correspondence
between executable (REPL) commands and library functions.
Inject any project path via the `pantograph_init_search` function.
## Developing
A Lean development shell is provided in the Nix flake.
### Testing
The tests are based on `LSpec`. To run tests, use either
``` sh
nix flake check
```
Then serve
```sh
cd docs/_build/html
python3 -m http.server -d .
or
``` sh
lake test
```
You can run an individual test by specifying a prefix
## Examples
For API interaction examples, see `examples/README.md`. The examples directory
also contains a comprehensive Jupyter notebook.
## Experiments
In `experiments/`, there are some experiments:
1. `minif2f` is an example of executing a `sglang` based prover on the miniF2F dataset
2. `dsp` is an Lean implementation of Draft-Sketch-Prove
The experiments should be run in `poetry shell`. The environment variable
`OPENAI_API_KEY` must be set when running experiments calling the OpenAI API.
## Referencing
[Paper Link](https://arxiv.org/abs/2410.16429)
```bib
@misc{pantograph,
title={Pantograph: A Machine-to-Machine Interaction Interface for Advanced Theorem Proving, High Level Reasoning, and Data Extraction in Lean 4},
author={Leni Aniva and Chuyue Sun and Brando Miranda and Clark Barrett and Sanmi Koyejo},
year={2024},
eprint={2410.16429},
archivePrefix={arXiv},
primaryClass={cs.LO},
url={https://arxiv.org/abs/2410.16429},
}
``` sh
lake test -- "Tactic/No Confuse"
```

307
Repl.lean Normal file
View File

@ -0,0 +1,307 @@
import Std.Data.HashMap
import Pantograph
namespace Pantograph.Repl
structure Context where
imports: List String
/-- Stores state of the REPL -/
structure State where
options: Protocol.Options := {}
nextId: Nat := 0
goalStates: Std.HashMap Nat GoalState := Std.HashMap.empty
/-- Main state monad for executing commands -/
abbrev MainM := ReaderT Context (StateT State Lean.CoreM)
def newGoalState (goalState: GoalState) : MainM Nat := do
let state ← get
let stateId := state.nextId
set { state with
goalStates := state.goalStates.insert stateId goalState,
nextId := state.nextId + 1
}
return stateId
-- HACK: For some reason writing `CommandM α := MainM (Except ... α)` disables
-- certain monadic features in `MainM`
abbrev CR α := Except Protocol.InteractionError α
def runMetaInMainM { α } (metaM: Lean.MetaM α): MainM α :=
metaM.run'
def runTermElabInMainM { α } (termElabM: Lean.Elab.TermElabM α) : MainM α :=
termElabM.run' (ctx := defaultElabContext) |>.run'
/-- Main loop command of the REPL -/
def execute (command: Protocol.Command): MainM Lean.Json := do
let run { α β: Type } [Lean.FromJson α] [Lean.ToJson β] (comm: α → MainM (CR β)): MainM Lean.Json :=
match Lean.fromJson? command.payload with
| .ok args => do
match (← comm args) with
| .ok result => return Lean.toJson result
| .error ierror => return Lean.toJson ierror
| .error error => return Lean.toJson $ errorCommand s!"Unable to parse json: {error}"
try
match command.cmd with
| "reset" => run reset
| "stat" => run stat
| "expr.echo" => run expr_echo
| "env.catalog" => run env_catalog
| "env.inspect" => run env_inspect
| "env.add" => run env_add
| "env.save" => run env_save
| "env.load" => run env_load
| "options.set" => run options_set
| "options.print" => run options_print
| "goal.start" => run goal_start
| "goal.tactic" => run goal_tactic
| "goal.continue" => run goal_continue
| "goal.delete" => run goal_delete
| "goal.print" => run goal_print
| "goal.save" => run goal_save
| "goal.load" => run goal_load
| "frontend.process" => run frontend_process
| cmd =>
let error: Protocol.InteractionError :=
errorCommand s!"Unknown command {cmd}"
return Lean.toJson error
catch ex => do
let error ← ex.toMessageData.toString
return Lean.toJson $ errorIO error
where
errorCommand := errorI "command"
errorIndex := errorI "index"
errorIO := errorI "io"
-- Command Functions
reset (_: Protocol.Reset): MainM (CR Protocol.StatResult) := do
let state ← get
let nGoals := state.goalStates.size
set { state with nextId := 0, goalStates := .empty }
Lean.Core.resetMessageLog
return .ok { nGoals }
stat (_: Protocol.Stat): MainM (CR Protocol.StatResult) := do
let state ← get
let nGoals := state.goalStates.size
return .ok { nGoals }
env_catalog (args: Protocol.EnvCatalog): MainM (CR Protocol.EnvCatalogResult) := do
let result ← Environment.catalog args
return .ok result
env_inspect (args: Protocol.EnvInspect): MainM (CR Protocol.EnvInspectResult) := do
let state ← get
Environment.inspect args state.options
env_add (args: Protocol.EnvAdd): MainM (CR Protocol.EnvAddResult) := do
Environment.addDecl args
env_save (args: Protocol.EnvSaveLoad): MainM (CR Protocol.EnvSaveLoadResult) := do
let env ← Lean.MonadEnv.getEnv
environmentPickle env args.path
return .ok {}
env_load (args: Protocol.EnvSaveLoad): MainM (CR Protocol.EnvSaveLoadResult) := do
let (env, _) ← environmentUnpickle args.path
Lean.setEnv env
return .ok {}
expr_echo (args: Protocol.ExprEcho): MainM (CR Protocol.ExprEchoResult) := do
let state ← get
exprEcho args.expr (expectedType? := args.type?) (levels := args.levels.getD #[]) (options := state.options)
options_set (args: Protocol.OptionsSet): MainM (CR Protocol.OptionsSetResult) := do
let state ← get
let options := state.options
set { state with
options := {
-- FIXME: This should be replaced with something more elegant
printJsonPretty := args.printJsonPretty?.getD options.printJsonPretty,
printExprPretty := args.printExprPretty?.getD options.printExprPretty,
printExprAST := args.printExprAST?.getD options.printExprAST,
printDependentMVars := args.printDependentMVars?.getD options.printDependentMVars,
noRepeat := args.noRepeat?.getD options.noRepeat,
printAuxDecls := args.printAuxDecls?.getD options.printAuxDecls,
printImplementationDetailHyps := args.printImplementationDetailHyps?.getD options.printImplementationDetailHyps
automaticMode := args.automaticMode?.getD options.automaticMode,
}
}
return .ok { }
options_print (_: Protocol.OptionsPrint): MainM (CR Protocol.Options) := do
return .ok (← get).options
goal_start (args: Protocol.GoalStart): MainM (CR Protocol.GoalStartResult) := do
let env ← Lean.MonadEnv.getEnv
let expr?: Except _ GoalState ← runTermElabInMainM (match args.expr, args.copyFrom with
| .some expr, .none => goalStartExpr expr (args.levels.getD #[])
| .none, .some copyFrom =>
(match env.find? <| copyFrom.toName with
| .none => return .error <| errorIndex s!"Symbol not found: {copyFrom}"
| .some cInfo => return .ok (← GoalState.create cInfo.type))
| _, _ =>
return .error <| errorI "arguments" "Exactly one of {expr, copyFrom} must be supplied")
match expr? with
| .error error => return .error error
| .ok goalState =>
let stateId ← newGoalState goalState
return .ok { stateId, root := goalState.root.name.toString }
goal_tactic (args: Protocol.GoalTactic): MainM (CR Protocol.GoalTacticResult) := do
let state ← get
let .some goalState := state.goalStates[args.stateId]? |
return .error $ errorIndex s!"Invalid state index {args.stateId}"
let .some goal := goalState.goals.get? args.goalId |
return .error $ errorIndex s!"Invalid goal index {args.goalId}"
let nextGoalState?: Except _ TacticResult ← runTermElabInMainM do
-- NOTE: Should probably use a macro to handle this...
match args.tactic?, args.expr?, args.have?, args.let?, args.calc?, args.conv?, args.draft? with
| .some tactic, .none, .none, .none, .none, .none, .none => do
pure <| Except.ok <| ← goalState.tryTactic goal tactic
| .none, .some expr, .none, .none, .none, .none, .none => do
pure <| Except.ok <| ← goalState.tryAssign goal expr
| .none, .none, .some type, .none, .none, .none, .none => do
let binderName := args.binderName?.getD ""
pure <| Except.ok <| ← goalState.tryHave goal binderName type
| .none, .none, .none, .some type, .none, .none, .none => do
let binderName := args.binderName?.getD ""
pure <| Except.ok <| ← goalState.tryLet goal binderName type
| .none, .none, .none, .none, .some pred, .none, .none => do
pure <| Except.ok <| ← goalState.tryCalc goal pred
| .none, .none, .none, .none, .none, .some true, .none => do
pure <| Except.ok <| ← goalState.conv goal
| .none, .none, .none, .none, .none, .some false, .none => do
pure <| Except.ok <| ← goalState.convExit
| .none, .none, .none, .none, .none, .none, .some draft => do
pure <| Except.ok <| ← goalState.tryDraft goal draft
| _, _, _, _, _, _, _ =>
let error := errorI "arguments" "Exactly one of {tactic, expr, have, calc, conv} must be supplied"
pure $ Except.error $ error
match nextGoalState? with
| .error error => return .error error
| .ok (.success nextGoalState) => do
let nextGoalState ← match state.options.automaticMode, args.conv? with
| true, .none => do
let .ok result := nextGoalState.resume (nextGoalState.goals ++ goalState.goals) |
throwError "Resuming known goals"
pure result
| true, .some true => pure nextGoalState
| true, .some false => do
let .some (_, _, dormantGoals) := goalState.convMVar? |
throwError "If conv exit succeeded this should not fail"
let .ok result := nextGoalState.resume (nextGoalState.goals ++ dormantGoals) |
throwError "Resuming known goals"
pure result
| false, _ => pure nextGoalState
let nextStateId ← newGoalState nextGoalState
let goals ← nextGoalState.serializeGoals (parent := .some goalState) (options := state.options) |>.run'
return .ok {
nextStateId? := .some nextStateId,
goals? := .some goals,
}
| .ok (.parseError message) =>
return .ok { parseError? := .some message }
| .ok (.invalidAction message) =>
return .error $ errorI "invalid" message
| .ok (.failure messages) =>
return .ok { tacticErrors? := .some messages }
goal_continue (args: Protocol.GoalContinue): MainM (CR Protocol.GoalContinueResult) := do
let state ← get
let .some target := state.goalStates[args.target]? |
return .error $ errorIndex s!"Invalid state index {args.target}"
let nextState? ← match args.branch?, args.goals? with
| .some branchId, .none => do
match state.goalStates[branchId]? with
| .none => return .error $ errorIndex s!"Invalid state index {branchId}"
| .some branch => pure $ target.continue branch
| .none, .some goals =>
pure $ goalResume target goals
| _, _ => return .error <| errorI "arguments" "Exactly one of {branch, goals} must be supplied"
match nextState? with
| .error error => return .error <| errorI "structure" error
| .ok nextGoalState =>
let nextStateId ← newGoalState nextGoalState
let goals ← goalSerialize nextGoalState (options := state.options)
return .ok {
nextStateId,
goals,
}
goal_delete (args: Protocol.GoalDelete): MainM (CR Protocol.GoalDeleteResult) := do
let state ← get
let goalStates := args.stateIds.foldl (λ map id => map.erase id) state.goalStates
set { state with goalStates }
return .ok {}
goal_print (args: Protocol.GoalPrint): MainM (CR Protocol.GoalPrintResult) := do
let state ← get
let .some goalState := state.goalStates[args.stateId]? |
return .error $ errorIndex s!"Invalid state index {args.stateId}"
let result ← runMetaInMainM <| goalPrint
goalState
(rootExpr := args.rootExpr?.getD False)
(parentExpr := args.parentExpr?.getD False)
(goals := args.goals?.getD False)
(extraMVars := args.extraMVars?.getD #[])
(options := state.options)
return .ok result
goal_save (args: Protocol.GoalSave): MainM (CR Protocol.GoalSaveResult) := do
let state ← get
let .some goalState := state.goalStates[args.id]? |
return .error $ errorIndex s!"Invalid state index {args.id}"
goalStatePickle goalState args.path
return .ok {}
goal_load (args: Protocol.GoalLoad): MainM (CR Protocol.GoalLoadResult) := do
let (goalState, _) ← goalStateUnpickle args.path (← Lean.MonadEnv.getEnv)
let id ← newGoalState goalState
return .ok { id }
frontend_process (args: Protocol.FrontendProcess): MainM (CR Protocol.FrontendProcessResult) := do
let options := (← get).options
try
let (fileName, file) ← match args.fileName?, args.file? with
| .some fileName, .none => do
let file ← IO.FS.readFile fileName
pure (fileName, file)
| .none, .some file =>
pure ("<anonymous>", file)
| _, _ => return .error <| errorI "arguments" "Exactly one of {fileName, file} must be supplied"
let env?: Option Lean.Environment ← if args.fileName?.isSome then
pure .none
else do
let env ← Lean.MonadEnv.getEnv
pure <| .some env
let (context, state) ← do Frontend.createContextStateFromFile file fileName env? {}
let frontendM := Frontend.mapCompilationSteps λ step => do
let boundary := (step.src.startPos.byteIdx, step.src.stopPos.byteIdx)
let invocations?: Option (List Protocol.InvokedTactic) ← if args.invocations then
let invocations ← Frontend.collectTacticsFromCompilationStep step
pure $ .some invocations
else
pure .none
let sorrys ← if args.sorrys then
Frontend.collectSorrys step (options := { collectTypeErrors := args.typeErrorsAsGoals })
else
pure []
let messages ← step.messageStrings
let newConstants ← if args.newConstants then
Frontend.collectNewDefinedConstants step
else
pure []
return (step.before, boundary, invocations?, sorrys, messages, newConstants)
let li ← frontendM.run context |>.run' state
let units ← li.mapM λ (env, boundary, invocations?, sorrys, messages, newConstants) => Lean.withEnv env do
let newConstants? := if args.newConstants then
.some $ newConstants.toArray.map λ name => name.toString
else
.none
let (goalStateId?, goals?, goalSrcBoundaries?) ← if sorrys.isEmpty then do
pure (.none, .none, .none)
else do
let { state, srcBoundaries } ← runMetaInMainM $ Frontend.sorrysToGoalState sorrys
let stateId ← newGoalState state
let goals ← goalSerialize state options
let srcBoundaries := srcBoundaries.toArray.map (λ (b, e) => (b.byteIdx, e.byteIdx))
pure (.some stateId, .some goals, .some srcBoundaries)
return {
boundary,
messages,
invocations?,
goalStateId?,
goals?,
goalSrcBoundaries?,
newConstants?,
}
return .ok { units }
catch e =>
return .error $ errorI "frontend" (← e.toMessageData.toString)
end Pantograph.Repl

150
SNAP.md
View File

@ -1,150 +0,0 @@
# Instructions for the SNAP Cluster
Brando's [self-contained `install.sh` script for
lean](https://github.com/brando90/learning_lean/blob/main/install.sh). (Warning:
The Lean version in the script is outdated.)
## Install 1: With Conda and Pip in the SNAP cluster
```bash
# - Install Lean 4 manually (elan & lake): gets install script (doesn't save it) & directly gives it to sh to install it
curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s -- -y
# (install command from the Lean4 official instlal guide, not the one we use)
# curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s
# - Make sure Lean4 tools (lean, lake) are available
echo $PATH | tr ':' '\n'
export PATH="$HOME/.elan/bin:$PATH"
echo 'export PATH="$HOME/.elan/bin:$PATH"' >> ~/.bashrc
# bash
elan
lake
# - Create and activate the right python env (this is needed so that poetry build works)
conda create -n pypantograph_env python=3.11 -y
conda activate pypantograph_env
#conda remove --name pypantograph_env --all
# - Install poetry with python venv (needs seperate install so poetry & your projs deps don't crash)
mkdir $HOME/.virtualenvs
# put the follow BEFORE your conda init stuff in your .bashrc
export VENV_PATH=$HOME/.virtualenvs/venv_for_poetry
export PATH="$VENV_PATH/bin:$PATH"
# now actually install poetry in a python env after creating an python env for poetry with venv
python3 -m venv $VENV_PATH
$VENV_PATH/bin/pip install -U pip setuptools
$VENV_PATH/bin/pip install poetry
poetry
# - Init the git submodules (i.e., make git aware of them/track them) + fetch/clone/update (and double check submodule is inited)
git submodule init
git submodule update --init
# - For snap make sure the repo is sym linked you're using your
git clone git@github.com:lenianiva/PyPantograph.git
# git checkout <your-branch>
git checkout brando
ln -s $AFS/PyPantograph $HOME/PyPantograph
# - Build the PyPantograph proj (build the py distribution, py deps and custom (lean4) installs). Note: pip install -e doesn't work on the dist .whl builds etc so you instead the next command
cd $HOME/PyPantograph
poetry build
```
To run server tests:
``` bash
python -m pantograph.server
python -m pantograph.search
```
The tests in `pantograph/server.py` also serve as simple interaction examples
# - Install pypantograph in editable mode (only pyproject.toml (or setup.py!) needed! Assuming your at the proj root)
cd $HOME/PyPantograph
pip install -e .
# - Confirm intalls
pip list | grep pantograph
pip list | grep vllm
pip list | grep torch
# - Make sure the PyPantrograph server tests by Leni work
cd ~/PyPantograph
python $HOME/PyPantograph/pantograph/server.py
python $HOME/PyPantograph/test_vllm.py
```
Note: the tests in `pantograph/server.py` also serve as simple interaction examples
References:
- My SNAP `.bashrc`: https://github.com/brando90/snap-cluster-setup/blob/main/.bashrc
- Especially useful for Conda vs Poetry export order
- Poetry in SNAP: https://github.com/brando90/snap-cluster-setup?tab=readme-ov-file#poetry
- Gitsubmodules: https://github.com/brando90/snap-cluster-setup?tab=readme-ov-file#git-submodules
- Lean in SNAP: https://github.com/brando90/snap-cluster-setup?tab=readme-ov-file#lean-in-snap
- ChatGPT: https://chat.openai.com/c/e01336a7-6f67-4cd2-b6cd-09b8ee8aef5a
# Install 2: With only Poetry in the SNAP cluster
```bash
# - Install Lean4 manually (elan and lake), 1st one is the SNAP one, 2nd is the most common one
curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s -- -y
# curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s
# - Make sure Lean4 tools (lean, lake) are available
export PATH="$HOME/.elan/bin:$PATH"
echo 'export PATH="$HOME/.elan/bin:$PATH"' >> ~/.bashrc
bash
elan
lake
# - Init the git submodules (i.e., make git aware of them/track them) + fetch/clone/update (and double check submodule is inited)
git submodule init
git submodule update --init --recursive
git clone git@github.com:lenianiva/PyPantograph.git --recurse-submodules
# - For snap make sure the repo is sym linked you're using your
git clone git@github.com:lenianiva/PyPantograph.git
git checkout <your-branch>
ln -s $AFS/PyPantograph $HOME/PyPantograph
# - Install poetry with python venv (needs seperate install so poetry & your projs deps don't crash)
mkdir $HOME/.virtualenvs
# - Put the follow BEFORE your conda init stuff in your .bashrc
export VENV_PATH=$HOME/.virtualenvs/venv_for_poetry
export PATH="$VENV_PATH/bin:$PATH"
# - Now actually install poetry in a python env after creating an python env for poetry with venv
python3 -m venv $VENV_PATH
$VENV_PATH/bin/pip install -U pip setuptools
$VENV_PATH/bin/pip install poetry
poetry
# poetry build is only needed when you build a python distribution e.g., .whl or .tar.gz and want to distribute it. You can't use those files for edtiable development anyway
# # - Build the PyPantograph proj (build the py distribution, py deps and custom (lean4) installs)
# cd $HOME/PyPantograph
# poetry build
# - Install pypantograph in editable mode with poetry
cd $HOME/PyPantograph
#Installs the project and its dependencies into the virtual environment, creating the environment if it doesn't exist, in editable mode. This will run our custom build for Lean already (the build.py file!)
poetry install
# if it create a new python env, check it out
poetry env list
# activate the current poetry env in a new shell
poetry shell
# - Confirm intalls
# poetry show | grep pantograph # note, doesn't do anything since poetry already only works by installing things in editable mode
poetry show | grep vllm
poetry show | grep torch
# - Make sure the PyPantrograph server tests by Leni work
cd ~/PyPantograph
python -m pantograph.server
# python $HOME/PyPantograph/pantograph/server.py
# python $HOME/PyPantograph/test_vllm.py

177
Test/Common.lean Normal file
View File

@ -0,0 +1,177 @@
import Pantograph.Goal
import Pantograph.Library
import Pantograph.Protocol
import Lean
import LSpec
open Lean
namespace Pantograph
deriving instance Repr for Expr
-- Use strict equality check for expressions
instance : BEq Expr := ⟨Expr.equal⟩
def uniq (n: Nat): Name := .num (.str .anonymous "_uniq") n
-- Auxiliary functions
namespace Protocol
def Goal.devolatilizeVars (goal: Goal): Goal :=
{
goal with
vars := goal.vars.map removeInternalAux,
}
where removeInternalAux (v: Variable): Variable :=
{
v with
name := ""
}
/-- Set internal names to "" -/
def Goal.devolatilize (goal: Goal): Goal :=
{
goal.devolatilizeVars with
name := "",
}
deriving instance DecidableEq, Repr for Name
deriving instance DecidableEq, Repr for Expression
deriving instance DecidableEq, Repr for Variable
deriving instance DecidableEq, Repr for Goal
deriving instance DecidableEq, Repr for ExprEchoResult
deriving instance DecidableEq, Repr for InteractionError
deriving instance DecidableEq, Repr for Option
end Protocol
namespace Condensed
deriving instance BEq, Repr for LocalDecl
deriving instance BEq, Repr for Goal
-- Enable string interpolation
instance : ToString FVarId where
toString id := id.name.toString
instance : ToString MVarId where
toString id := id.name.toString
protected def LocalDecl.devolatilize (decl: LocalDecl): LocalDecl :=
{
decl with fvarId := { name := .anonymous }
}
protected def Goal.devolatilize (goal: Goal): Goal :=
{
goal with
mvarId := { name := .anonymous },
context := goal.context.map LocalDecl.devolatilize
}
end Condensed
def GoalState.get! (state: GoalState) (i: Nat): MVarId := state.goals.get! i
def GoalState.tacticOn (state: GoalState) (goalId: Nat) (tactic: String) := state.tryTactic (state.goals.get! goalId) tactic
def TacticResult.toString : TacticResult → String
| .success state => s!".success ({state.goals.length} goals)"
| .failure messages =>
let messages := "\n".intercalate messages.toList
s!".failure {messages}"
| .parseError error => s!".parseError {error}"
| .invalidAction error => s!".invalidAction {error}"
namespace Test
def expectationFailure (desc: String) (error: String): LSpec.TestSeq := LSpec.test desc (LSpec.ExpectationFailure "ok _" error)
def assertUnreachable (message: String): LSpec.TestSeq := LSpec.check message false
def parseFailure (error: String) := expectationFailure "parse" error
def elabFailure (error: String) := expectationFailure "elab" error
def runCoreMSeq (env: Environment) (coreM: CoreM LSpec.TestSeq) (options: Array String := #[]): IO LSpec.TestSeq := do
let coreContext: Core.Context ← createCoreContext options
match ← (coreM.run' coreContext { env := env }).toBaseIO with
| .error exception =>
return LSpec.test "Exception" (s!"internal exception #{← exception.toMessageData.toString}" = "")
| .ok a => return a
def runMetaMSeq (env: Environment) (metaM: MetaM LSpec.TestSeq): IO LSpec.TestSeq :=
runCoreMSeq env metaM.run'
def runTermElabMInMeta { α } (termElabM: Lean.Elab.TermElabM α): Lean.MetaM α :=
termElabM.run' (ctx := defaultElabContext)
def runTermElabMSeq (env: Environment) (termElabM: Elab.TermElabM LSpec.TestSeq): IO LSpec.TestSeq :=
runMetaMSeq env $ termElabM.run' (ctx := defaultElabContext)
def exprToStr (e: Expr): Lean.MetaM String := toString <$> Meta.ppExpr e
def strToTermSyntax (s: String): CoreM Syntax := do
let .ok stx := Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := s)
(fileName := ← getFileName) | panic! s!"Failed to parse {s}"
return stx
def parseSentence (s: String): Elab.TermElabM Expr := do
let stx ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := s)
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
Elab.Term.elabTerm (stx := stx) .none
def runTacticOnMVar (tacticM: Elab.Tactic.TacticM Unit) (goal: MVarId): Elab.TermElabM (List MVarId) := do
let (_, newGoals) ← tacticM { elaborator := .anonymous } |>.run { goals := [goal] }
return newGoals.goals
def mvarUserNameAndType (mvarId: MVarId): MetaM (Name × String) := do
let name := (← mvarId.getDecl).userName
let t ← exprToStr (← mvarId.getType)
return (name, t)
-- Monadic testing
abbrev TestT := StateRefT' IO.RealWorld LSpec.TestSeq
section Monadic
variable [Monad m] [MonadLiftT (ST IO.RealWorld) m]
def addTest (test: LSpec.TestSeq) : TestT m Unit := do
set $ (← get) ++ test
def checkEq [DecidableEq α] [Repr α] (desc : String) (lhs rhs : α) : TestT m Unit := do
addTest $ LSpec.check desc (lhs = rhs)
def checkTrue (desc : String) (flag : Bool) : TestT m Unit := do
addTest $ LSpec.check desc flag
def fail (desc : String) : TestT m Unit := do
addTest $ LSpec.check desc false
def runTest (t: TestT m Unit): m LSpec.TestSeq :=
Prod.snd <$> t.run LSpec.TestSeq.done
def runTestWithResult { α } (t: TestT m α): m (α × LSpec.TestSeq) :=
t.run LSpec.TestSeq.done
def runTestCoreM (env: Environment) (coreM: TestT CoreM Unit) (options: Array String := #[]): IO LSpec.TestSeq := do
runCoreMSeq env (runTest coreM) options
end Monadic
def runTestTermElabM (env: Environment) (t: TestT Elab.TermElabM Unit):
IO LSpec.TestSeq :=
runTermElabMSeq env $ runTest t
def cdeclOf (userName: Name) (type: Expr): Condensed.LocalDecl :=
{ userName, type }
def buildGoal (nameType: List (String × String)) (target: String) (userName?: Option String := .none):
Protocol.Goal :=
{
userName?,
target := { pp? := .some target},
vars := (nameType.map fun x => ({
userName := x.fst,
type? := .some { pp? := .some x.snd },
})).toArray
}
end Test
end Pantograph

109
Test/Delate.lean Normal file
View File

@ -0,0 +1,109 @@
import LSpec
import Pantograph.Delate
import Test.Common
import Lean
open Lean
namespace Pantograph.Test.Delate
open Pantograph
deriving instance Repr, DecidableEq for Protocol.BoundExpression
def test_serializeName: LSpec.TestSeq :=
let quote := "\""
let escape := "\\"
LSpec.test "a.b.1" (serializeName (Name.num (.str (.str .anonymous "a") "b") 1) = "a.b.1") ++
LSpec.test "seg.«a.b»" (serializeName (Name.str (.str .anonymous "seg") "a.b") = s!"{quote}seg.«a.b»{quote}") ++
-- Pathological test case
LSpec.test s!"«̈{escape}{quote}»" (serializeName (Name.str .anonymous s!"{escape}{quote}") = s!"{quote}«{escape}{quote}»{quote}")
def test_expr_to_binder (env: Environment): IO LSpec.TestSeq := do
let entries: List (Name × Protocol.BoundExpression) := [
("Nat.add_comm".toName, { binders := #[("n", "Nat"), ("m", "Nat")], target := "n + m = m + n" }),
("Nat.le_of_succ_le".toName, { binders := #[("n", "Nat"), ("m", "Nat"), ("h", "n.succ ≤ m")], target := "n ≤ m" })
]
runCoreMSeq env $ entries.foldlM (λ suites (symbol, target) => do
let env ← MonadEnv.getEnv
let expr := env.find? symbol |>.get! |>.type
let test := LSpec.check symbol.toString ((← typeExprToBound expr) = target)
return LSpec.TestSeq.append suites test) LSpec.TestSeq.done |>.run'
def test_sexp_of_symbol (env: Environment): IO LSpec.TestSeq := do
let entries: List (String × String) := [
-- This one contains unhygienic variable names which must be suppressed
("Nat.add", "(:forall a (:c Nat) (:forall a (:c Nat) (:c Nat)))"),
-- These ones are normal and easy
("Nat.add_one", "(:forall n (:c Nat) ((:c Eq) (:c Nat) ((:c HAdd.hAdd) (:c Nat) (:c Nat) (:c Nat) ((:c instHAdd) (:c Nat) (:c instAddNat)) 0 ((:c OfNat.ofNat) (:c Nat) (:lit 1) ((:c instOfNatNat) (:lit 1)))) ((:c Nat.succ) 0)))"),
("Nat.le_of_succ_le", "(:forall n (:c Nat) (:forall m (:c Nat) (:forall h ((:c LE.le) (:c Nat) (:c instLENat) ((:c Nat.succ) 1) 0) ((:c LE.le) (:c Nat) (:c instLENat) 2 1)) :i) :i)"),
-- Handling of higher order types
("Or", "(:forall a (:sort 0) (:forall b (:sort 0) (:sort 0)))"),
("List", "(:forall α (:sort (+ u 1)) (:sort (+ u 1)))")
]
runMetaMSeq env $ entries.foldlM (λ suites (symbol, target) => do
let env ← MonadEnv.getEnv
let expr := env.find? symbol.toName |>.get! |>.type
let test := LSpec.check symbol ((← serializeExpressionSexp expr) = target)
return LSpec.TestSeq.append suites test) LSpec.TestSeq.done
def test_sexp_of_elab (env: Environment): IO LSpec.TestSeq := do
let entries: List (String × (List Name) × String) := [
("λ x: Nat × Bool => x.1", [], "(:lambda x ((:c Prod) (:c Nat) (:c Bool)) ((:c Prod.fst) (:c Nat) (:c Bool) 0))"),
("λ x: Array Nat => x.data", [], "(:lambda x ((:c Array) (:c Nat)) ((:c Array.data) (:c Nat) 0))"),
("λ {α: Sort (u + 1)} => List α", [`u], "(:lambda α (:sort (+ u 1)) ((:c List) 0) :i)"),
("λ {α} => List α", [], "(:lambda α (:sort (+ (:mv _uniq.4) 1)) ((:c List) 0) :i)"),
("(2: Nat) <= (5: Nat)", [], "((:c LE.le) (:mv _uniq.18) (:mv _uniq.19) ((:c OfNat.ofNat) (:mv _uniq.4) (:lit 2) (:mv _uniq.5)) ((:c OfNat.ofNat) (:mv _uniq.14) (:lit 5) (:mv _uniq.15)))"),
]
entries.foldlM (λ suites (source, levels, target) =>
let termElabM := do
let env ← MonadEnv.getEnv
let s ← match parseTerm env source with
| .ok s => pure s
| .error e => return parseFailure e
let expr ← match (← elabTerm s) with
| .ok expr => pure expr
| .error e => return elabFailure e
return LSpec.check source ((← serializeExpressionSexp expr) = target)
let metaM := (Elab.Term.withLevelNames levels termElabM).run' (ctx := defaultElabContext)
return LSpec.TestSeq.append suites (← runMetaMSeq env metaM))
LSpec.TestSeq.done
def test_sexp_of_expr (env: Environment): IO LSpec.TestSeq := do
let entries: List (Expr × String) := [
(.lam `p (.sort .zero)
(.lam `q (.sort .zero)
(.lam `k (mkApp2 (.const `And []) (.bvar 1) (.bvar 0))
(.proj `And 1 (.bvar 0))
.default)
.implicit)
.implicit,
"(:lambda p (:sort 0) (:lambda q (:sort 0) (:lambda k ((:c And) 1 0) ((:c And.right) _ _ 0)) :i) :i)"
),
]
let termElabM: Elab.TermElabM LSpec.TestSeq := entries.foldlM (λ suites (expr, target) => do
let env ← MonadEnv.getEnv
let testCaseName := target.take 10
let test := LSpec.check testCaseName ((← serializeExpressionSexp expr) = target)
return LSpec.TestSeq.append suites test) LSpec.TestSeq.done
runMetaMSeq env $ termElabM.run' (ctx := defaultElabContext)
-- Instance parsing
def test_instance (env: Environment): IO LSpec.TestSeq :=
runMetaMSeq env do
let env ← MonadEnv.getEnv
let source := "λ x y: Nat => HAdd.hAdd Nat Nat Nat (instHAdd Nat instAddNat) x y"
let s := parseTerm env source |>.toOption |>.get!
let _expr := (← runTermElabMInMeta <| elabTerm s) |>.toOption |>.get!
return LSpec.TestSeq.done
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[
("serializeName", do pure test_serializeName),
("Expression binder", test_expr_to_binder env),
("Sexp from symbol", test_sexp_of_symbol env),
("Sexp from elaborated expr", test_sexp_of_elab env),
("Sexp from expr", test_sexp_of_expr env),
("Instance", test_instance env),
]
end Pantograph.Test.Delate

122
Test/Environment.lean Normal file
View File

@ -0,0 +1,122 @@
import LSpec
import Pantograph.Delate
import Pantograph.Environment
import Test.Common
import Lean
open Lean
namespace Pantograph.Test.Environment
open Pantograph
deriving instance DecidableEq, Repr for Protocol.InductInfo
deriving instance DecidableEq, Repr for Protocol.ConstructorInfo
deriving instance DecidableEq, Repr for Protocol.RecursorRule
deriving instance DecidableEq, Repr for Protocol.RecursorInfo
deriving instance DecidableEq, Repr for Protocol.EnvInspectResult
def test_catalog: IO LSpec.TestSeq := do
let env: Environment ← importModules
(imports := #[`Init])
(opts := {})
(trustLevel := 1)
let inner: CoreM LSpec.TestSeq := do
let catalogResult ← Environment.catalog {}
let symbolsWithNum := env.constants.fold (init := #[]) (λ acc name info =>
match (Environment.toFilteredSymbol name info).isSome && (name matches .num _ _) with
| false => acc
| true => acc.push name
)
return LSpec.check "No num symbols" (symbolsWithNum.size == 0)
runCoreMSeq env inner
def test_symbol_visibility: IO LSpec.TestSeq := do
let entries: List (Name × Bool) := [
("Nat.add_comm".toName, false),
("foo.bla.Init.Data.List.Basic.2.1.Init.Lean.Expr._hyg.4".toName, true),
("Init.Data.Nat.Basic._auxLemma.4".toName, true),
]
let suite := entries.foldl (λ suites (symbol, target) =>
let test := LSpec.check symbol.toString ((Environment.isNameInternal symbol) == target)
LSpec.TestSeq.append suites test) LSpec.TestSeq.done
return suite
inductive ConstantCat where
| induct (info: Protocol.InductInfo)
| ctor (info: Protocol.ConstructorInfo)
| recursor (info: Protocol.RecursorInfo)
def test_inspect: IO LSpec.TestSeq := do
let env: Environment ← importModules
(imports := #[`Init])
(opts := {})
(trustLevel := 1)
let testCases: List (String × ConstantCat) := [
("Or", ConstantCat.induct {
numParams := 2,
numIndices := 0,
all := #["Or"],
ctors := #["Or.inl", "Or.inr"],
}),
("Except.ok", ConstantCat.ctor {
induct := "Except",
cidx := 1,
numParams := 2,
numFields := 1,
}),
("Eq.rec", ConstantCat.recursor {
all := #["Eq"],
numParams := 2,
numIndices := 1,
numMotives := 1,
numMinors := 1,
rules := #[{ ctor := "Eq.refl", nFields := 0, rhs := { pp? := .some "fun {α} a motive refl => refl" } }]
k := true,
}),
("ForM.rec", ConstantCat.recursor {
all := #["ForM"],
numParams := 3,
numIndices := 0,
numMotives := 1,
numMinors := 1,
rules := #[{ ctor := "ForM.mk", nFields := 1, rhs := { pp? := .some "fun m γ α motive mk forM => mk forM" } }]
k := false,
})
]
let inner: CoreM LSpec.TestSeq := do
testCases.foldlM (λ acc (name, cat) => do
let args: Protocol.EnvInspect := { name := name }
let result ← match ← Environment.inspect args (options := {}) with
| .ok result => pure $ result
| .error e => panic! s!"Error: {e.desc}"
let p := match cat with
| .induct info => LSpec.test name (result.inductInfo? == .some info)
| .ctor info => LSpec.test name (result.constructorInfo? == .some info)
| .recursor info => LSpec.test name (result.recursorInfo? == .some info)
return LSpec.TestSeq.append acc p
) LSpec.TestSeq.done
runCoreMSeq env inner
def test_symbol_location : TestT IO Unit := do
let env: Environment ← importModules
(imports := #[`Init])
(opts := {})
(trustLevel := 1)
addTest $ ← runTestCoreM env do
let .ok result ← Environment.inspect { name := "Nat.le_of_succ_le", source? := .some true } (options := {}) | fail "Inspect failed"
checkEq "module" result.module? <| .some "Init.Data.Nat.Basic"
-- Extraction of source doesn't work for symbols in `Init` for some reason
checkTrue "file" result.sourceUri?.isNone
checkEq "pos" (result.sourceStart?.map (·.column)) <| .some 0
checkEq "pos" (result.sourceEnd?.map (·.column)) <| .some 88
def suite: List (String × IO LSpec.TestSeq) :=
[
("Catalog", test_catalog),
("Symbol Visibility", test_symbol_visibility),
("Inspect", test_inspect),
("Symbol Location", runTest test_symbol_location),
]
end Pantograph.Test.Environment

248
Test/Frontend.lean Normal file
View File

@ -0,0 +1,248 @@
import LSpec
import Pantograph
import Repl
import Test.Common
open Lean Pantograph
namespace Pantograph.Test.Frontend
def collectSorrysFromSource (source: String) (options : Frontend.GoalCollectionOptions := {})
: MetaM (List GoalState) := do
let filename := "<anonymous>"
let (context, state) ← do Frontend.createContextStateFromFile source filename (← getEnv) {}
let m := Frontend.mapCompilationSteps λ step => do
return (step.before, ← Frontend.collectSorrys step options)
let li ← m.run context |>.run' state
let goalStates ← li.filterMapM λ (env, sorrys) => withEnv env do
if sorrys.isEmpty then
return .none
let { state, .. } ← Frontend.sorrysToGoalState sorrys
return .some state
return goalStates
def test_multiple_sorrys_in_proof : TestT MetaM Unit := do
let sketch := "
theorem plus_n_Sm_proved_formal_sketch : ∀ n m : Nat, n + (m + 1) = (n + m) + 1 := by
have h_nat_add_succ: ∀ n m : Nat, n = m := sorry
sorry
"
let goalStates ← (collectSorrysFromSource sketch).run' {}
let [goalState] := goalStates | panic! "Incorrect number of states"
addTest $ LSpec.check "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize) = #[
{
target := { pp? := "∀ (n m : Nat), n = m" },
vars := #[
]
},
{
target := { pp? := "∀ (n m : Nat), n + (m + 1) = n + m + 1" },
vars := #[{
userName := "h_nat_add_succ",
type? := .some { pp? := "∀ (n m : Nat), n = m" },
}],
}
])
def test_sorry_in_middle: TestT MetaM Unit := do
let sketch := "
example : ∀ (n m: Nat), n + m = m + n := by
intros n m
sorry
"
let goalStates ← (collectSorrysFromSource sketch).run' {}
let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
addTest $ LSpec.check "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize) = #[
{
target := { pp? := "n + m = m + n" },
vars := #[{
userName := "n",
type? := .some { pp? := "Nat" },
}, {
userName := "m",
type? := .some { pp? := "Nat" },
}
],
}
])
def test_sorry_in_induction : TestT MetaM Unit := do
let sketch := "
example : ∀ (n m: Nat), n + m = m + n := by
intros n m
induction n with
| zero =>
have h1 : 0 + m = m := sorry
sorry
| succ n ih =>
have h2 : n + m = m := sorry
sorry
"
let goalStates ← (collectSorrysFromSource sketch).run' {}
let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
addTest $ LSpec.check "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize) = #[
{
target := { pp? := "0 + m = m" },
vars := #[{
userName := "m",
type? := .some { pp? := "Nat" },
}]
},
{
userName? := .some "zero",
target := { pp? := "0 + m = m + 0" },
vars := #[{
userName := "m",
type? := .some { pp? := "Nat" },
}, {
userName := "h1",
type? := .some { pp? := "0 + m = m" },
}]
},
{
target := { pp? := "n + m = m" },
vars := #[{
userName := "m",
type? := .some { pp? := "Nat" },
}, {
userName := "n",
type? := .some { pp? := "Nat" },
}, {
userName := "ih",
type? := .some { pp? := "n + m = m + n" },
}]
},
{
userName? := .some "succ",
target := { pp? := "n + 1 + m = m + (n + 1)" },
vars := #[{
userName := "m",
type? := .some { pp? := "Nat" },
}, {
userName := "n",
type? := .some { pp? := "Nat" },
}, {
userName := "ih",
type? := .some { pp? := "n + m = m + n" },
}, {
userName := "h2",
type? := .some { pp? := "n + m = m" },
}]
}
])
def test_sorry_in_coupled: TestT MetaM Unit := do
let sketch := "
example : ∀ (y: Nat), ∃ (x: Nat), y + 1 = x := by
intro y
apply Exists.intro
case h => sorry
case w => sorry
"
let goalStates ← (collectSorrysFromSource sketch).run' {}
let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
addTest $ LSpec.check "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize) = #[
{
target := { pp? := "y + 1 = ?w" },
vars := #[{
userName := "y",
type? := .some { pp? := "Nat" },
}
],
},
{
userName? := .some "w",
target := { pp? := "Nat" },
vars := #[{
userName := "y",
type? := .some { pp? := "Nat" },
}
],
}
])
def test_environment_capture: TestT MetaM Unit := do
let sketch := "
def mystery (n: Nat) := n + 1
example (n: Nat) : mystery n + 1 = n + 2 := sorry
"
let goalStates ← (collectSorrysFromSource sketch).run' {}
let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
addTest $ LSpec.check "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize) = #[
{
target := { pp? := "mystery n + 1 = n + 2" },
vars := #[{
userName := "n",
type? := .some { pp? := "Nat" },
}],
}
])
def test_capture_type_mismatch : TestT MetaM Unit := do
let input := "
def mystery (k: Nat) : Nat := true
"
let options := { collectTypeErrors := true }
let goalStates ← (collectSorrysFromSource input options).run' {}
let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
checkEq "goals" ((← goalState.serializeGoals).map (·.devolatilize)) #[
{
target := { pp? := "Nat" },
vars := #[{
userName := "k",
type? := .some { pp? := "Nat" },
}],
}
]
def test_capture_type_mismatch_in_binder : TestT MetaM Unit := do
let input := "
example (p: Prop) (h: (∀ (x: Prop), Nat) → p): p := h (λ (y: Nat) => 5)
"
let options := { collectTypeErrors := true }
let goalStates ← (collectSorrysFromSource input options).run' {}
let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}"
checkEq "goals" ((← goalState.serializeGoals (options := {})).map (·.devolatilize)) #[
]
def collectNewConstants (source: String) : MetaM (List (List Name)) := do
let filename := "<anonymous>"
let (context, state) ← do Frontend.createContextStateFromFile source filename (← getEnv) {}
let m := Frontend.mapCompilationSteps λ step => do
Frontend.collectNewDefinedConstants step
m.run context |>.run' state
def test_collect_one_constant : TestT MetaM Unit := do
let input := "
def mystery : Nat := 123
"
let names ← collectNewConstants input
checkEq "constants" names [[`mystery]]
def test_collect_one_theorem : TestT MetaM Unit := do
let input := "
theorem mystery [SizeOf α] (as : List α) (i : Fin as.length) : sizeOf (as.get i) < sizeOf as := by
match as, i with
| a::as, ⟨0, _⟩ => simp_arith [get]
| a::as, ⟨i+1, h⟩ =>
have ih := sizeOf_get as ⟨i, Nat.le_of_succ_le_succ h⟩
apply Nat.lt_trans ih
simp_arith
"
let names ← collectNewConstants input
checkEq "constants" names [[`mystery]]
def suite (env : Environment): List (String × IO LSpec.TestSeq) :=
let tests := [
("multiple_sorrys_in_proof", test_multiple_sorrys_in_proof),
("sorry_in_middle", test_sorry_in_middle),
("sorry_in_induction", test_sorry_in_induction),
("sorry_in_coupled", test_sorry_in_coupled),
("environment_capture", test_environment_capture),
("capture_type_mismatch", test_capture_type_mismatch),
--("capture_type_mismatch_in_binder", test_capture_type_mismatch_in_binder),
("collect_one_constant", test_collect_one_constant),
("collect_one_theorem", test_collect_one_theorem),
]
tests.map (fun (name, test) => (name, runMetaMSeq env $ runTest test))
end Pantograph.Test.Frontend

263
Test/Integration.lean Normal file
View File

@ -0,0 +1,263 @@
/- Integration test for the REPL
-/
import LSpec
import Pantograph
import Repl
import Test.Common
namespace Pantograph.Test.Integration
open Pantograph.Repl
def step { α } [Lean.ToJson α] (cmd: String) (payload: List (String × Lean.Json))
(expected: α) (name? : Option String := .none): MainM LSpec.TestSeq := do
let payload := Lean.Json.mkObj payload
let name := name?.getD s!"{cmd} {payload.compress}"
let result ← Repl.execute { cmd, payload }
return LSpec.test name (toString result = toString (Lean.toJson expected))
abbrev Test := List (MainM LSpec.TestSeq)
def test_elab : Test :=
[
step "expr.echo"
[("expr", .str "λ {α : Sort (u + 1)} => List α"), ("levels", .arr #["u"])]
(Lean.toJson ({
type := { pp? := .some "{α : Type u} → Type u" },
expr := { pp? := .some "fun {α} => List α" }
}: Protocol.ExprEchoResult)),
]
def test_option_modify : Test :=
let pp? := Option.some "∀ (n : Nat), n + 1 = n.succ"
let sexp? := Option.some "(:forall n (:c Nat) ((:c Eq) (:c Nat) ((:c HAdd.hAdd) (:c Nat) (:c Nat) (:c Nat) ((:c instHAdd) (:c Nat) (:c instAddNat)) 0 ((:c OfNat.ofNat) (:c Nat) (:lit 1) ((:c instOfNatNat) (:lit 1)))) ((:c Nat.succ) 0)))"
let module? := Option.some "Init.Data.Nat.Basic"
let options: Protocol.Options := {}
[
step "env.inspect" [("name", .str "Nat.add_one")]
({ type := { pp? }, module? }: Protocol.EnvInspectResult),
step "options.set" [("printExprAST", .bool true)]
({ }: Protocol.OptionsSetResult),
step "env.inspect" [("name", .str "Nat.add_one")]
({ type := { pp?, sexp? }, module? }: Protocol.EnvInspectResult),
step "options.print" []
({ options with printExprAST := true }: Protocol.Options),
]
def test_malformed_command : Test :=
let invalid := "invalid"
[
step invalid [("name", .str "Nat.add_one")]
({ error := "command", desc := s!"Unknown command {invalid}" }: Protocol.InteractionError)
(name? := .some "Invalid Command"),
step "expr.echo" [(invalid, .str "Random garbage data")]
({ error := "command", desc := s!"Unable to parse json: Pantograph.Protocol.ExprEcho.expr: String expected" }:
Protocol.InteractionError)
(name? := .some "JSON Deserialization")
]
def test_tactic : Test :=
let goal1: Protocol.Goal := {
name := "_uniq.11",
target := { pp? := .some "∀ (q : Prop), x q → q x" },
vars := #[{ name := "_uniq.10", userName := "x", type? := .some { pp? := .some "Prop" }}],
}
let goal2: Protocol.Goal := {
name := "_uniq.17",
target := { pp? := .some "x y → y x" },
vars := #[
{ name := "_uniq.10", userName := "x", type? := .some { pp? := .some "Prop" }},
{ name := "_uniq.16", userName := "y", type? := .some { pp? := .some "Prop" }}
],
}
[
step "goal.start" [("expr", .str "∀ (p q: Prop), p q → q p")]
({ stateId := 0, root := "_uniq.9" }: Protocol.GoalStartResult),
step "goal.tactic" [("stateId", .num 0), ("goalId", .num 0), ("tactic", .str "intro x")]
({ nextStateId? := .some 1, goals? := #[goal1], }: Protocol.GoalTacticResult),
step "goal.print" [("stateId", .num 1), ("parentExpr", .bool true), ("rootExpr", .bool true)]
({ parent? := .some { pp? := .some "fun x => ?m.11" }, }: Protocol.GoalPrintResult),
step "goal.tactic" [("stateId", .num 1), ("goalId", .num 0), ("tactic", .str "intro y")]
({ nextStateId? := .some 2, goals? := #[goal2], }: Protocol.GoalTacticResult),
]
def test_automatic_mode (automatic: Bool): Test :=
let varsPQ := #[
{ name := "_uniq.10", userName := "p", type? := .some { pp? := .some "Prop" }},
{ name := "_uniq.13", userName := "q", type? := .some { pp? := .some "Prop" }}
]
let goal1: Protocol.Goal := {
name := "_uniq.17",
target := { pp? := .some "q p" },
vars := varsPQ ++ #[
{ name := "_uniq.16", userName := "h", type? := .some { pp? := .some "p q" }}
],
}
let goal2l: Protocol.Goal := {
name := "_uniq.61",
userName? := .some "inl",
target := { pp? := .some "q p" },
vars := varsPQ ++ #[
{ name := "_uniq.49", userName := "h✝", type? := .some { pp? := .some "p" }, isInaccessible := true}
],
}
let goal2r: Protocol.Goal := {
name := "_uniq.74",
userName? := .some "inr",
target := { pp? := .some "q p" },
vars := varsPQ ++ #[
{ name := "_uniq.62", userName := "h✝", type? := .some { pp? := .some "q" }, isInaccessible := true}
],
}
let goal3l: Protocol.Goal := {
name := "_uniq.80",
userName? := .some "inl.h",
target := { pp? := .some "p" },
vars := varsPQ ++ #[
{ name := "_uniq.49", userName := "h✝", type? := .some { pp? := .some "p" }, isInaccessible := true}
],
}
[
step "options.set" [("automaticMode", .bool automatic)]
({}: Protocol.OptionsSetResult),
step "goal.start" [("expr", .str "∀ (p q: Prop), p q → q p")]
({ stateId := 0, root := "_uniq.9" }: Protocol.GoalStartResult),
step "goal.tactic" [("stateId", .num 0), ("goalId", .num 0), ("tactic", .str "intro p q h")]
({ nextStateId? := .some 1, goals? := #[goal1], }: Protocol.GoalTacticResult),
step "goal.tactic" [("stateId", .num 1), ("goalId", .num 0), ("tactic", .str "cases h")]
({ nextStateId? := .some 2, goals? := #[goal2l, goal2r], }: Protocol.GoalTacticResult),
let goals? := if automatic then #[goal3l, goal2r] else #[goal3l]
step "goal.tactic" [("stateId", .num 2), ("goalId", .num 0), ("tactic", .str "apply Or.inr")]
({ nextStateId? := .some 3, goals?, }: Protocol.GoalTacticResult),
]
def test_env_add_inspect : Test :=
let name1 := "Pantograph.mystery"
let name2 := "Pantograph.mystery2"
[
step "env.add"
[
("name", .str name1),
("type", .str "Prop → Prop → Prop"),
("value", .str "λ (a b: Prop) => Or a b"),
("isTheorem", .bool false)
]
({}: Protocol.EnvAddResult),
step "env.inspect" [("name", .str name1)]
({
value? := .some { pp? := .some "fun a b => a b" },
type := { pp? := .some "Prop → Prop → Prop" },
}:
Protocol.EnvInspectResult),
step "env.add"
[
("name", .str name2),
("type", .str "Nat → Int"),
("value", .str "λ (a: Nat) => a + 1"),
("isTheorem", .bool false)
]
({}: Protocol.EnvAddResult),
step "env.inspect" [("name", .str name2)]
({
value? := .some { pp? := .some "fun a => ↑a + 1" },
type := { pp? := .some "Nat → Int" },
}:
Protocol.EnvInspectResult)
]
example : ∀ (p: Prop), p → p := by
intro p h
exact h
def test_frontend_process : Test :=
[
let file := "example : ∀ (p q: Prop), p → p q := by\n intro p q h\n exact Or.inl h"
let goal1 := "p q : Prop\nh : p\n⊢ p q"
step "frontend.process"
[
("file", .str file),
("invocations", .bool true),
("sorrys", .bool false),
("typeErrorsAsGoals", .bool false),
("newConstants", .bool false),
]
({
units := [{
boundary := (0, file.utf8ByteSize),
invocations? := .some [
{
goalBefore := "⊢ ∀ (p q : Prop), p → p q",
goalAfter := goal1,
tactic := "intro p q h",
usedConstants := #[],
},
{
goalBefore := goal1 ,
goalAfter := "",
tactic := "exact Or.inl h",
usedConstants := #["Or.inl"],
},
]
}],
}: Protocol.FrontendProcessResult),
]
example : 1 + 2 = 3 := rfl
example (p: Prop): p → p := by simp
def test_frontend_process_sorry : Test :=
let solved := "example : 1 + 2 = 3 := rfl\n"
let withSorry := "example (p: Prop): p → p := sorry"
[
let file := s!"{solved}{withSorry}"
let goal1: Protocol.Goal := {
name := "_uniq.6",
target := { pp? := .some "p → p" },
vars := #[{ name := "_uniq.4", userName := "p", type? := .some { pp? := .some "Prop" }}],
}
step "frontend.process"
[
("file", .str file),
("invocations", .bool false),
("sorrys", .bool true),
("typeErrorsAsGoals", .bool false),
("newConstants", .bool false),
]
({
units := [{
boundary := (0, solved.utf8ByteSize),
}, {
boundary := (solved.utf8ByteSize, solved.utf8ByteSize + withSorry.utf8ByteSize),
goalStateId? := .some 0,
goals? := .some #[goal1],
goalSrcBoundaries? := .some #[(57, 62)],
messages := #["<anonymous>:2:0: warning: declaration uses 'sorry'\n"],
}],
}: Protocol.FrontendProcessResult),
]
def runTest (env: Lean.Environment) (steps: Test): IO LSpec.TestSeq := do
-- Setup the environment for execution
let context: Context := {
imports := ["Init"]
}
let commands: MainM LSpec.TestSeq :=
steps.foldlM (λ suite step => do
let result ← step
return suite ++ result) LSpec.TestSeq.done
runCoreMSeq env <| commands.run context |>.run' {}
def suite (env : Lean.Environment): List (String × IO LSpec.TestSeq) :=
let tests := [
("expr.echo", test_elab),
("options.set options.print", test_option_modify),
("Malformed command", test_malformed_command),
("Tactic", test_tactic),
("Manual Mode", test_automatic_mode false),
("Automatic Mode", test_automatic_mode true),
("env.add env.inspect", test_env_add_inspect),
("frontend.process invocation", test_frontend_process),
("frontend.process sorry", test_frontend_process_sorry),
]
tests.map (fun (name, test) => (name, runTest env test))
end Pantograph.Test.Integration

38
Test/Library.lean Normal file
View File

@ -0,0 +1,38 @@
import LSpec
import Lean
import Pantograph.Library
import Test.Common
open Lean
open Pantograph
namespace Pantograph.Test.Library
def test_expr_echo (env: Environment): IO LSpec.TestSeq := do
let inner: CoreM LSpec.TestSeq := do
let prop_and_proof := "⟨∀ (x: Prop), x → x, λ (x: Prop) (h: x) => h⟩"
let tests := LSpec.TestSeq.done
let echoResult ← exprEcho prop_and_proof (options := {})
let tests := tests.append (LSpec.test "fail" (echoResult.toOption == .some {
type := { pp? := "?m.2" }, expr := { pp? := "?m.3" }
}))
let echoResult ← exprEcho prop_and_proof (expectedType? := .some "Σ' p:Prop, p") (options := { printExprAST := true })
let tests := tests.append (LSpec.test "fail" (echoResult.toOption == .some {
type := {
pp? := "(p : Prop) ×' p",
sexp? := "((:c PSigma) (:sort 0) (:lambda p (:sort 0) 0))",
},
expr := {
pp? := "⟨∀ (x : Prop), x → x, fun x h => h⟩",
sexp? := "((:c PSigma.mk) (:sort 0) (:lambda p (:sort 0) 0) (:forall x (:sort 0) (:forall a 0 1)) (:lambda x (:sort 0) (:lambda h 0 0)))",
}
}))
return tests
runCoreMSeq env (options := #["pp.proofs.threshold=100"]) inner
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[
("expr_echo", test_expr_echo env),
]
end Pantograph.Test.Library

63
Test/Main.lean Normal file
View File

@ -0,0 +1,63 @@
import LSpec
import Test.Delate
import Test.Environment
import Test.Frontend
import Test.Integration
import Test.Library
import Test.Metavar
import Test.Proofs
import Test.Serial
import Test.Tactic
-- Test running infrastructure
namespace Pantograph.Test
def addPrefix (pref: String) (tests: List (String × α)): List (String × α) :=
tests.map (λ (name, x) => (pref ++ "/" ++ name, x))
/-- Runs test in parallel. Filters test name if given -/
def runTestGroup (filter: Option String) (tests: List (String × IO LSpec.TestSeq)): IO LSpec.TestSeq := do
let tests: List (String × IO LSpec.TestSeq) := match filter with
| .some filter => tests.filter (λ (name, _) => filter.isPrefixOf name)
| .none => tests
let tasks: List (String × Task _) ← tests.mapM (λ (name, task) => do
return (name, ← EIO.asTask task))
let all := tasks.foldl (λ acc (name, task) =>
let v: Except IO.Error LSpec.TestSeq := Task.get task
match v with
| .ok case => acc ++ (LSpec.group name case)
| .error e => acc ++ (expectationFailure name e.toString)
) LSpec.TestSeq.done
return all
end Pantograph.Test
open Pantograph.Test
/-- Main entry of tests; Provide an argument to filter tests by prefix -/
def main (args: List String) := do
let name_filter := args.head?
Lean.initSearchPath (← Lean.findSysroot)
let env_default: Lean.Environment ← Lean.importModules
(imports := #[`Init])
(opts := {})
(trustLevel := 1)
let suites: List (String × List (String × IO LSpec.TestSeq)) := [
("Environment", Environment.suite),
("Frontend", Frontend.suite env_default),
("Integration", Integration.suite env_default),
("Library", Library.suite env_default),
("Metavar", Metavar.suite env_default),
("Proofs", Proofs.suite env_default),
("Delate", Delate.suite env_default),
("Serial", Serial.suite env_default),
("Tactic/Assign", Tactic.Assign.suite env_default),
("Tactic/Congruence", Tactic.Congruence.suite env_default),
("Tactic/Motivated Apply", Tactic.MotivatedApply.suite env_default),
("Tactic/No Confuse", Tactic.NoConfuse.suite env_default),
("Tactic/Prograde", Tactic.Prograde.suite env_default),
]
let tests: List (String × IO LSpec.TestSeq) := suites.foldl (λ acc (name, suite) => acc ++ (addPrefix name suite)) []
LSpec.lspecIO (← runTestGroup name_filter tests)

280
Test/Metavar.lean Normal file
View File

@ -0,0 +1,280 @@
import LSpec
import Pantograph.Goal
import Pantograph.Delate
import Test.Common
import Lean
namespace Pantograph.Test.Metavar
open Pantograph
open Lean
abbrev TestM := TestT $ ReaderT Protocol.Options Elab.TermElabM
-- Tests that all delay assigned mvars are instantiated
def test_instantiate_mvar: TestM Unit := do
let env ← Lean.MonadEnv.getEnv
let value := "@Nat.le_trans 2 2 5 (@of_eq_true (@LE.le Nat instLENat 2 2) (@eq_true (@LE.le Nat instLENat 2 2) (@Nat.le_refl 2))) (@of_eq_true (@LE.le Nat instLENat 2 5) (@eq_true_of_decide (@LE.le Nat instLENat 2 5) (@Nat.decLe 2 5) (@Eq.refl Bool Bool.true)))"
let syn ← match parseTerm env value with
| .ok s => pure $ s
| .error e => do
addTest $ assertUnreachable e
return ()
let expr ← match ← elabTerm syn with
| .ok expr => pure $ expr
| .error e => do
addTest $ assertUnreachable e
return ()
let t ← Lean.Meta.inferType expr
addTest $ LSpec.check "typing" ((toString (← serializeExpressionSexp t)) =
"((:c LE.le) (:c Nat) (:c instLENat) ((:c OfNat.ofNat) (:mv _uniq.2) (:lit 2) (:mv _uniq.3)) ((:c OfNat.ofNat) (:mv _uniq.14) (:lit 5) (:mv _uniq.15)))")
return ()
def startProof (expr: String): TestM (Option GoalState) := do
let env ← Lean.MonadEnv.getEnv
let syn? := parseTerm env expr
addTest $ LSpec.check s!"Parsing {expr}" (syn?.isOk)
match syn? with
| .error error =>
IO.println error
return Option.none
| .ok syn =>
let expr? ← elabType syn
addTest $ LSpec.check s!"Elaborating" expr?.isOk
match expr? with
| .error error =>
IO.println error
return Option.none
| .ok expr =>
let goal ← GoalState.create (expr := expr)
return Option.some goal
def buildGoal (nameType: List (String × String)) (target: String) (userName?: Option String := .none): Protocol.Goal :=
{
userName?,
target := { pp? := .some target},
vars := (nameType.map fun x => ({
userName := x.fst,
type? := .some { pp? := .some x.snd },
})).toArray
}
def proofRunner (env: Lean.Environment) (tests: TestM Unit): IO LSpec.TestSeq := do
let termElabM := tests.run LSpec.TestSeq.done |>.run {} -- with default options
let coreContext: Lean.Core.Context ← createCoreContext #[]
let metaM := termElabM.run' (ctx := defaultElabContext)
let coreM := metaM.run'
match ← (coreM.run' coreContext { env := env }).toBaseIO with
| .error exception =>
return LSpec.test "Exception" (s!"internal exception #{← exception.toMessageData.toString}" = "")
| .ok (_, a) =>
return a
/-- M-coupled goals -/
def test_m_couple: TestM Unit := do
let state? ← startProof "(2: Nat) ≤ 5"
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := "apply Nat.le_trans") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "apply Nat.le_trans" ((← state1.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "2 ≤ ?m", .some "?m ≤ 5", .some "Nat"])
addTest $ LSpec.test "(1 root)" state1.rootExpr?.isNone
-- Set m to 3
let state2 ← match ← state1.tacticOn (goalId := 2) (tactic := "exact 3") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.test "(1b root)" state2.rootExpr?.isNone
let state1b ← match state2.continue state1 with
| .error msg => do
addTest $ assertUnreachable $ msg
return ()
| .ok state => pure state
addTest $ LSpec.check "exact 3" ((← state1b.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "2 ≤ 3", .some "3 ≤ 5"])
addTest $ LSpec.test "(2 root)" state1b.rootExpr?.isNone
def test_m_couple_simp: TestM Unit := do
let state? ← startProof "(2: Nat) ≤ 5"
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := "apply Nat.le_trans") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let serializedState1 ← state1.serializeGoals (options := { ← read with printDependentMVars := true })
addTest $ LSpec.check "apply Nat.le_trans" (serializedState1.map (·.target.pp?) =
#[.some "2 ≤ ?m", .some "?m ≤ 5", .some "Nat"])
addTest $ LSpec.check "(metavariables)" (serializedState1.map (·.target.dependentMVars?.get!) =
#[#["_uniq.38"], #["_uniq.38"], #[]])
let state2 ← match ← state1.tacticOn (goalId := 2) (tactic := "exact 2") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.test "(1b root)" state2.rootExpr?.isNone
let state1b ← match state2.continue state1 with
| .error msg => do
addTest $ assertUnreachable $ msg
return ()
| .ok state => pure state
addTest $ LSpec.check "exact 2" ((← state1b.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "2 ≤ 2", .some "2 ≤ 5"])
addTest $ LSpec.test "(2 root)" state1b.rootExpr?.isNone
let state3 ← match ← state1b.tacticOn (goalId := 0) (tactic := "simp") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let state4 ← match state3.continue state1b with
| .error msg => do
addTest $ assertUnreachable $ msg
return ()
| .ok state => pure state
let state5 ← match ← state4.tacticOn (goalId := 0) (tactic := "simp") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
state5.restoreMetaM
let root ← match state5.rootExpr? with
| .some e => pure e
| .none =>
addTest $ assertUnreachable "(5 root)"
return ()
let rootStr: String := toString (← Lean.Meta.ppExpr root)
addTest $ LSpec.check "(5 root)" (rootStr = "Nat.le_trans (of_eq_true (Init.Data.Nat.Basic._auxLemma.4 2)) (of_eq_true (eq_true_of_decide (Eq.refl true)))")
let unfoldedRoot ← unfoldAuxLemmas root
addTest $ LSpec.check "(5 root)" ((toString (← Lean.Meta.ppExpr unfoldedRoot)) =
"Nat.le_trans (of_eq_true (eq_true (Nat.le_refl 2))) (of_eq_true (eq_true_of_decide (Eq.refl true)))")
return ()
def test_proposition_generation: TestM Unit := do
let state? ← startProof "Σ' p:Prop, p"
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := "apply PSigma.mk") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "apply PSigma.mk" ((← state1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[
buildGoal [] "?fst" (userName? := .some "snd"),
buildGoal [] "Prop" (userName? := .some "fst")
])
if let #[goal1, goal2] := ← state1.serializeGoals (options := { (← read) with printExprAST := true }) then
addTest $ LSpec.test "(1 reference)" (goal1.target.sexp? = .some s!"(:mv {goal2.name})")
addTest $ LSpec.test "(1 root)" state1.rootExpr?.isNone
let state2 ← match ← state1.tryAssign (state1.get! 0) (expr := "λ (x: Nat) => _") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check ":= λ (x: Nat), _" ((← state2.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "?m.29 x"])
addTest $ LSpec.test "(2 root)" state2.rootExpr?.isNone
let assign := "Eq.refl x"
let state3 ← match ← state2.tryAssign (state2.get! 0) (expr := assign) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!":= {assign}" ((← state3.serializeGoals (options := ← read)).map (·.target.pp?) =
#[])
addTest $ LSpec.test "(3 root)" state3.rootExpr?.isSome
return ()
def test_partial_continuation: TestM Unit := do
let state? ← startProof "(2: Nat) ≤ 5"
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := "apply Nat.le_trans") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "apply Nat.le_trans" ((← state1.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "2 ≤ ?m", .some "?m ≤ 5", .some "Nat"])
let state2 ← match ← state1.tacticOn (goalId := 2) (tactic := "apply Nat.succ") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "apply Nat.succ" ((← state2.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "Nat"])
-- Execute a partial continuation
let coupled_goals := state1.goals ++ state2.goals
let state1b ← match state2.resume (goals := coupled_goals) with
| .error msg => do
addTest $ assertUnreachable $ msg
return ()
| .ok state => pure state
addTest $ LSpec.check "(continue)" ((← state1b.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "2 ≤ Nat.succ ?m", .some "Nat.succ ?m ≤ 5", .some "Nat"])
addTest $ LSpec.test "(2 root)" state1b.rootExpr?.isNone
-- Roundtrip
--let coupled_goals := coupled_goals.map (λ g =>
-- { name := str_to_name $ serializeName g.name (sanitize := false)})
let coupled_goals := coupled_goals.map (λ g => serializeName g.name (sanitize := false))
let coupled_goals := coupled_goals.map (λ g => { name := g.toName })
let state1b ← match state2.resume (goals := coupled_goals) with
| .error msg => do
addTest $ assertUnreachable $ msg
return ()
| .ok state => pure state
addTest $ LSpec.check "(continue)" ((← state1b.serializeGoals (options := ← read)).map (·.target.pp?) =
#[.some "2 ≤ Nat.succ ?m", .some "Nat.succ ?m ≤ 5", .some "Nat"])
addTest $ LSpec.test "(2 root)" state1b.rootExpr?.isNone
-- Continuation should fail if the state does not exist:
match state0.resume coupled_goals with
| .error error => addTest $ LSpec.check "(continuation failure message)" (error = "Goals [_uniq.40, _uniq.41, _uniq.38, _uniq.47] are not in scope")
| .ok _ => addTest $ assertUnreachable "(continuation failure)"
-- Continuation should fail if some goals have not been solved
match state2.continue state1 with
| .error error => addTest $ LSpec.check "(continuation failure message)" (error = "Target state has unresolved goals")
| .ok _ => addTest $ assertUnreachable "(continuation failure)"
return ()
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
let tests := [
("Instantiate", test_instantiate_mvar),
("2 < 5", test_m_couple),
("2 < 5", test_m_couple_simp),
("Proposition Generation", test_proposition_generation),
("Partial Continuation", test_partial_continuation)
]
tests.map (fun (name, test) => (name, proofRunner env test))
end Pantograph.Test.Metavar

792
Test/Proofs.lean Normal file
View File

@ -0,0 +1,792 @@
/-
Tests pertaining to goals with no interdependencies
-/
import LSpec
import Pantograph.Goal
import Pantograph.Delate
import Test.Common
namespace Pantograph.Test.Proofs
open Pantograph
open Lean
inductive Start where
| copy (name: String) -- Start from some name in the environment
| expr (expr: String) -- Start from some expression
abbrev TestM := TestT $ ReaderT Protocol.Options $ Elab.TermElabM
def startProof (start: Start): TestM (Option GoalState) := do
let env ← Lean.MonadEnv.getEnv
match start with
| .copy name =>
let cInfo? := name.toName |> env.find?
addTest $ LSpec.check s!"Symbol exists {name}" cInfo?.isSome
match cInfo? with
| .some cInfo =>
let goal ← GoalState.create (expr := cInfo.type)
return Option.some goal
| .none =>
return Option.none
| .expr expr =>
let syn? := parseTerm env expr
addTest $ LSpec.check s!"Parsing {expr}" (syn?.isOk)
match syn? with
| .error error =>
IO.println error
return Option.none
| .ok syn =>
let expr? ← elabType syn
addTest $ LSpec.check s!"Elaborating" expr?.isOk
match expr? with
| .error error =>
IO.println error
return Option.none
| .ok expr =>
let goal ← GoalState.create (expr := expr)
return Option.some goal
def buildNamedGoal (name: String) (nameType: List (String × String)) (target: String)
(userName?: Option String := .none): Protocol.Goal :=
{
name,
userName?,
target := { pp? := .some target},
vars := (nameType.map fun x => ({
userName := x.fst,
type? := .some { pp? := .some x.snd },
})).toArray
}
def buildGoal (nameType: List (String × String)) (target: String) (userName?: Option String := .none):
Protocol.Goal :=
{
userName?,
target := { pp? := .some target},
vars := (nameType.map fun x => ({
userName := x.fst,
type? := .some { pp? := .some x.snd },
})).toArray
}
def proofRunner (env: Lean.Environment) (tests: TestM Unit): IO LSpec.TestSeq := do
let termElabM := tests.run LSpec.TestSeq.done |>.run {} -- with default options
let coreContext: Lean.Core.Context ← createCoreContext #[]
let metaM := termElabM.run' (ctx := defaultElabContext)
let coreM := metaM.run'
match ← (coreM.run' coreContext { env := env }).toBaseIO with
| .error exception =>
return LSpec.test "Exception" (s!"internal exception #{← exception.toMessageData.toString}" = "")
| .ok (_, a) =>
return a
def test_identity: TestM Unit := do
let state? ← startProof (.expr "∀ (p: Prop), p → p")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intro p h"
let state1 ← match ← state0.tacticOn 0 tactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let inner := "_uniq.12"
addTest $ LSpec.check tactic ((← state1.serializeGoals (options := ← read)).map (·.name) =
#[inner])
let state1parent ← state1.withParentContext do
serializeExpressionSexp (← instantiateAll state1.parentExpr?.get!)
addTest $ LSpec.test "(1 parent)" (state1parent == s!"(:lambda p (:sort 0) (:lambda h 0 (:subst (:mv {inner}) 1 0)))")
-- Individual test cases
example: ∀ (a b: Nat), a + b = b + a := by
intro n m
rw [Nat.add_comm]
def test_nat_add_comm (manual: Bool): TestM Unit := do
let state? ← startProof <| match manual with
| false => .copy "Nat.add_comm"
| true => .expr "∀ (a b: Nat), a + b = b + a"
addTest $ LSpec.check "Start goal" state?.isSome
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let state1 ← match ← state0.tacticOn 0 "intro n m" with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "intro n m" ((← state1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[buildGoal [("n", "Nat"), ("m", "Nat")] "n + m = m + n"])
match ← state1.tacticOn 0 "assumption" with
| .failure #[message] =>
addTest $ LSpec.check "assumption" (message = "tactic 'assumption' failed\nn m : Nat\n⊢ n + m = m + n")
| other => do
addTest $ assertUnreachable $ other.toString
let state2 ← match ← state1.tacticOn 0 "rw [Nat.add_comm]" with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.test "rw [Nat.add_comm]" state2.goals.isEmpty
return ()
def test_delta_variable: TestM Unit := do
let options: Protocol.Options := { noRepeat := true }
let state? ← startProof <| .expr "∀ (a b: Nat), a + b = b + a"
addTest $ LSpec.check "Start goal" state?.isSome
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := "intro n") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "intro n" ((← state1.serializeGoals (parent := state0) options).map (·.devolatilize) =
#[buildGoalSelective [("n", .some "Nat")] "∀ (b : Nat), n + b = b + n"])
let state2 ← match ← state1.tacticOn (goalId := 0) (tactic := "intro m") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "intro m" ((← state2.serializeGoals (parent := state1) options).map (·.devolatilize) =
#[buildGoalSelective [("n", .none), ("m", .some "Nat")] "n + m = m + n"])
return ()
where
-- Like `buildGoal` but allow certain variables to be elided.
buildGoalSelective (nameType: List (String × Option String)) (target: String): Protocol.Goal :=
{
target := { pp? := .some target},
vars := (nameType.map fun x => ({
userName := x.fst,
type? := x.snd.map (λ type => { pp? := type }),
})).toArray
}
example (w x y z : Nat) (p : Nat → Prop)
(h : p (x * y + z * w * x)) : p (x * w * z + y * x) := by
simp [Nat.add_assoc, Nat.add_comm, Nat.add_left_comm, Nat.mul_comm, Nat.mul_assoc, Nat.mul_left_comm] at *
assumption
def test_arith: TestM Unit := do
let state? ← startProof (.expr "∀ (w x y z : Nat) (p : Nat → Prop) (h : p (x * y + z * w * x)), p (x * w * z + y * x)")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intros"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic (state1.goals.length = 1)
addTest $ LSpec.test "(1 root)" state1.rootExpr?.isNone
let state2 ← match ← state1.tacticOn (goalId := 0) (tactic := "simp [Nat.add_assoc, Nat.add_comm, Nat.add_left_comm, Nat.mul_comm, Nat.mul_assoc, Nat.mul_left_comm] at *") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "simp ..." (state2.goals.length = 1)
addTest $ LSpec.check "(2 root)" state2.rootExpr?.isNone
let tactic := "assumption"
let state3 ← match ← state2.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.test tactic state3.goals.isEmpty
addTest $ LSpec.check "(3 root)" state3.rootExpr?.isSome
return ()
-- Two ways to write the same theorem
example: ∀ (p q: Prop), p q → q p := by
intro p q h
cases h
apply Or.inr
assumption
apply Or.inl
assumption
example: ∀ (p q: Prop), p q → q p := by
intro p q h
cases h
. apply Or.inr
assumption
. apply Or.inl
assumption
def test_or_comm: TestM Unit := do
let state? ← startProof (.expr "∀ (p q: Prop), p q → q p")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
addTest $ LSpec.check "(0 parent)" state0.parentExpr?.isNone
addTest $ LSpec.check "(0 root)" state0.rootExpr?.isNone
let tactic := "intro p q h"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let [state1g0] := state1.goals | fail "Should have 1 goal"
let (fvP, fvQ, fvH) ← state1.withContext state1g0 do
let lctx ← getLCtx
let #[fvP, fvQ, fvH] := lctx.getFVarIds.map (toString ·.name) |
panic! "Incorrect number of decls"
pure (fvP, fvQ, fvH)
addTest $ LSpec.check tactic ((← state1.serializeGoals (options := ← read)) =
#[{
name := state1g0.name.toString,
target := { pp? := .some "q p" },
vars := #[
{ name := fvP, userName := "p", type? := .some { pp? := .some "Prop" } },
{ name := fvQ, userName := "q", type? := .some { pp? := .some "Prop" } },
{ name := fvH, userName := "h", type? := .some { pp? := .some "p q" } }
]
}])
addTest $ LSpec.check "(1 parent)" state1.parentExpr?.isSome
addTest $ LSpec.check "(1 root)" state1.rootExpr?.isNone
let state1parent ← state1.withParentContext do
serializeExpressionSexp (← instantiateAll state1.parentExpr?.get!)
addTest $ LSpec.test "(1 parent)" (state1parent == s!"(:lambda p (:sort 0) (:lambda q (:sort 0) (:lambda h ((:c Or) 1 0) (:subst (:mv {state1g0}) 2 1 0))))")
let tactic := "cases h"
let state2 ← match ← state1.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state2.serializeGoals (options := ← read)).map (·.devolatilize) =
#[branchGoal "inl" "p", branchGoal "inr" "q"])
let [state2g0, state2g1] := state2.goals |
fail s!"Should have 2 goals, but it has {state2.goals.length}"
let (caseL, caseR) := (state2g0.name.toString, state2g1.name.toString)
addTest $ LSpec.check tactic ((← state2.serializeGoals (options := ← read)).map (·.name) =
#[caseL, caseR])
addTest $ LSpec.check "(2 parent exists)" state2.parentExpr?.isSome
addTest $ LSpec.check "(2 root)" state2.rootExpr?.isNone
let state2parent ← state2.withParentContext do
serializeExpressionSexp (← instantiateAll state2.parentExpr?.get!)
let orPQ := s!"((:c Or) (:fv {fvP}) (:fv {fvQ}))"
let orQP := s!"((:c Or) (:fv {fvQ}) (:fv {fvP}))"
let motive := s!"(:lambda t {orPQ} (:forall h ((:c Eq) ((:c Or) (:fv {fvP}) (:fv {fvQ})) (:fv {fvH}) 0) {orQP}))"
let caseL := s!"(:lambda h (:fv {fvP}) (:lambda h ((:c Eq) {orPQ} (:fv {fvH}) ((:c Or.inl) (:fv {fvP}) (:fv {fvQ}) 0)) (:subst (:mv {caseL}) (:fv {fvP}) (:fv {fvQ}) 1)))"
let caseR := s!"(:lambda h (:fv {fvQ}) (:lambda h ((:c Eq) {orPQ} (:fv {fvH}) ((:c Or.inr) (:fv {fvP}) (:fv {fvQ}) 0)) (:subst (:mv {caseR}) (:fv {fvP}) (:fv {fvQ}) 1)))"
let conduit := s!"((:c Eq.refl) {orPQ} (:fv {fvH}))"
addTest $ LSpec.test "(2 parent)" (state2parent ==
s!"((:c Or.casesOn) (:fv {fvP}) (:fv {fvQ}) {motive} (:fv {fvH}) {caseL} {caseR} {conduit})")
let state3_1 ← match ← state2.tacticOn (goalId := 0) (tactic := "apply Or.inr") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let state3_1parent ← state3_1.withParentContext do
serializeExpressionSexp (← instantiateAll state3_1.parentExpr?.get!)
let [state3_1goal0] := state3_1.goals | fail "Should have 1 goal"
addTest $ LSpec.test "(3_1 parent)" (state3_1parent == s!"((:c Or.inr) (:fv {fvQ}) (:fv {fvP}) (:mv {state3_1goal0}))")
addTest $ LSpec.check "· apply Or.inr" (state3_1.goals.length = 1)
let state4_1 ← match ← state3_1.tacticOn (goalId := 0) (tactic := "assumption") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check " assumption" state4_1.goals.isEmpty
let state4_1parent ← instantiateAll state4_1.parentExpr?.get!
addTest $ LSpec.test "(4_1 parent)" state4_1parent.isFVar
addTest $ LSpec.check "(4_1 root)" state4_1.rootExpr?.isNone
let state3_2 ← match ← state2.tacticOn (goalId := 1) (tactic := "apply Or.inl") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "· apply Or.inl" (state3_2.goals.length = 1)
let state4_2 ← match ← state3_2.tacticOn (goalId := 0) (tactic := "assumption") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check " assumption" state4_2.goals.isEmpty
addTest $ LSpec.check "(4_2 root)" state4_2.rootExpr?.isNone
-- Ensure the proof can continue from `state4_2`.
let state2b ← match state4_2.continue state2 with
| .error msg => do
addTest $ assertUnreachable $ msg
return ()
| .ok state => pure state
addTest $ LSpec.test "(resume)" (state2b.goals == [state2.goals.get! 0])
let state3_1 ← match ← state2b.tacticOn (goalId := 0) (tactic := "apply Or.inr") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "· apply Or.inr" (state3_1.goals.length = 1)
let state4_1 ← match ← state3_1.tacticOn (goalId := 0) (tactic := "assumption") with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check " assumption" state4_1.goals.isEmpty
addTest $ LSpec.check "(4_1 root)" state4_1.rootExpr?.isSome
return ()
where
typeProp: Protocol.Expression := { pp? := .some "Prop" }
branchGoal (caseName varName: String): Protocol.Goal := {
userName? := .some caseName,
target := { pp? := .some "q p" },
vars := #[
{ userName := "p", type? := .some typeProp },
{ userName := "q", type? := .some typeProp },
{ userName := "h✝", type? := .some { pp? := .some varName }, isInaccessible := true }
]
}
example : ∀ (a b c1 c2: Nat), (b + a) + c1 = (b + a) + c2 → (a + b) + c1 = (b + a) + c2 := by
intro a b c1 c2 h
conv =>
lhs
congr
. rw [Nat.add_comm]
. rfl
exact h
def test_conv: TestM Unit := do
let state? ← startProof (.expr "∀ (a b c1 c2: Nat), (b + a) + c1 = (b + a) + c2 → (a + b) + c1 = (b + a) + c2")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intro a b c1 c2 h"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[interiorGoal [] "a + b + c1 = b + a + c2"])
let state2 ← match ← state1.conv (state1.get! 0) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check "conv => ..." ((← state2.serializeGoals (options := ← read)).map (·.devolatilize) =
#[{ interiorGoal [] "a + b + c1 = b + a + c2" with isConversion := true }])
let convTactic := "rhs"
let state3R ← match ← state2.tacticOn (goalId := 0) convTactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!" {convTactic} (discard)" ((← state3R.serializeGoals (options := ← read)).map (·.devolatilize) =
#[{ interiorGoal [] "b + a + c2" with isConversion := true }])
let convTactic := "lhs"
let state3L ← match ← state2.tacticOn (goalId := 0) convTactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!" {convTactic}" ((← state3L.serializeGoals (options := ← read)).map (·.devolatilize) =
#[{ interiorGoal [] "a + b + c1" with isConversion := true }])
let convTactic := "congr"
let state4 ← match ← state3L.tacticOn (goalId := 0) convTactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!" {convTactic}" ((← state4.serializeGoals (options := ← read)).map (·.devolatilize) =
#[
{ interiorGoal [] "a + b" with isConversion := true, userName? := .some "a" },
{ interiorGoal [] "c1" with isConversion := true, userName? := .some "a" }
])
let convTactic := "rw [Nat.add_comm]"
let state5_1 ← match ← state4.tacticOn (goalId := 0) convTactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!" · {convTactic}" ((← state5_1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[{ interiorGoal [] "b + a" with isConversion := true, userName? := .some "a" }])
let convTactic := "rfl"
let state6_1 ← match ← state5_1.tacticOn (goalId := 0) convTactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!" {convTactic}" ((← state6_1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[])
let state4_1 ← match state6_1.continue state4 with
| .ok state => pure state
| .error e => do
addTest $ expectationFailure "continue" e
return ()
let convTactic := "rfl"
let state6 ← match ← state4_1.tacticOn (goalId := 0) convTactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!" · {convTactic}" ((← state6.serializeGoals (options := ← read)).map (·.devolatilize) =
#[])
let state1_1 ← match ← state6.convExit with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let tactic := "exact h"
let stateF ← match ← state1_1.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← stateF.serializeGoals (options := ← read)).map (·.devolatilize) =
#[])
where
h := "b + a + c1 = b + a + c2"
interiorGoal (free: List (String × String)) (target: String) :=
let free := [("a", "Nat"), ("b", "Nat"), ("c1", "Nat"), ("c2", "Nat"), ("h", h)] ++ free
buildGoal free target
example : ∀ (a b c d: Nat), a + b = b + c → b + c = c + d → a + b = c + d := by
intro a b c d h1 h2
calc a + b = b + c := by apply h1
_ = c + d := by apply h2
def test_calc: TestM Unit := do
let state? ← startProof (.expr "∀ (a b c d: Nat), a + b = b + c → b + c = c + d → a + b = c + d")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intro a b c d h1 h2"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[interiorGoal [] "a + b = c + d"])
let pred := "a + b = b + c"
let state2 ← match ← state1.tryCalc (state1.get! 0) (pred := pred) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!"calc {pred} := _" ((← state2.serializeGoals (options := ← read)).map (·.devolatilize) =
#[
interiorGoal [] "a + b = b + c" (.some "calc"),
interiorGoal [] "b + c = c + d"
])
addTest $ LSpec.test "(2.0 prev rhs)" (state2.calcPrevRhsOf? (state2.get! 0) |>.isNone)
addTest $ LSpec.test "(2.1 prev rhs)" (state2.calcPrevRhsOf? (state2.get! 1) |>.isSome)
let tactic := "apply h1"
let state2m ← match ← state2.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let state3 ← match state2m.continue state2 with
| .ok state => pure state
| .error e => do
addTest $ expectationFailure "continue" e
return ()
let pred := "_ = c + d"
let state4 ← match ← state3.tryCalc (state3.get! 0) (pred := pred) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!"calc {pred} := _" ((← state4.serializeGoals (options := ← read)).map (·.devolatilize) =
#[
interiorGoal [] "b + c = c + d" (.some "calc")
])
addTest $ LSpec.test "(4.0 prev rhs)" (state4.calcPrevRhsOf? (state4.get! 0) |>.isNone)
let tactic := "apply h2"
let state4m ← match ← state4.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.test "(4m root)" state4m.rootExpr?.isSome
where
interiorGoal (free: List (String × String)) (target: String) (userName?: Option String := .none) :=
let free := [("a", "Nat"), ("b", "Nat"), ("c", "Nat"), ("d", "Nat"),
("h1", "a + b = b + c"), ("h2", "b + c = c + d")] ++ free
buildGoal free target userName?
def test_nat_zero_add: TestM Unit := do
let state? ← startProof (.expr "∀ (n: Nat), n + 0 = n")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intro n"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[buildGoal [("n", "Nat")] "n + 0 = n"])
let recursor := "@Nat.brecOn"
let state2 ← match ← state1.tryMotivatedApply (state1.get! 0) (recursor := recursor) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let [mvarMotive, mvarMajor, mvarInduct, mvarConduit] := state2.goals |
fail "Incorrect number of goals"
let .num _ major := mvarMajor.name | fail "Incorrect form of mvar id"
addTest $ LSpec.check s!"mapply {recursor}" ((← state2.serializeGoals (options := ← read)).map (·.devolatilizeVars) =
#[
buildNamedGoal mvarMotive.name.toString [("n", "Nat")] "Nat → Prop" (.some "motive"),
buildNamedGoal mvarMajor.name.toString [("n", "Nat")] "Nat",
buildNamedGoal mvarInduct.name.toString [("n", "Nat")] "∀ (t : Nat), Nat.below t → ?motive t",
buildNamedGoal mvarConduit.name.toString [("n", "Nat")] s!"?motive ?m.{major} = (n + 0 = n)" (.some "conduit")
])
let tactic := "exact n"
let state3b ← match ← state2.tacticOn (goalId := 1) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state3b.serializeGoals (options := ← read)).map (·.devolatilize) =
#[])
let state2b ← match state3b.continue state2 with
| .ok state => pure state
| .error e => do
addTest $ assertUnreachable e
return ()
let tactic := "exact (λ x => x + 0 = x)"
let state3c ← match ← state2b.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state3c.serializeGoals (options := ← read)).map (·.devolatilize) =
#[])
let state2c ← match state3c.continue state2b with
| .ok state => pure state
| .error e => do
addTest $ assertUnreachable e
return ()
let tactic := "intro t h"
let state3 ← match ← state2c.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state3.serializeGoals (options := ← read)).map (·.devolatilize) =
#[buildGoal [("n", "Nat"), ("t", "Nat"), ("h", "Nat.below t")] "t + 0 = t"])
let tactic := "simp"
let state3d ← match ← state3.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let state2d ← match state3d.continue state2c with
| .ok state => pure state
| .error e => do
addTest $ assertUnreachable e
return ()
let tactic := "rfl"
let stateF ← match ← state2d.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← stateF.serializeGoals (options := ← read)) =
#[])
let expr := stateF.mctx.eAssignment.find! stateF.root
let (expr, _) := instantiateMVarsCore (mctx := stateF.mctx) (e := expr)
addTest $ LSpec.check "(F root)" stateF.rootExpr?.isSome
def test_nat_zero_add_alt: TestM Unit := do
let state? ← startProof (.expr "∀ (n: Nat), n + 0 = n")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intro n"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state1.serializeGoals (options := ← read)).map (·.devolatilize) =
#[buildGoal [("n", "Nat")] "n + 0 = n"])
let recursor := "@Nat.brecOn"
let state2 ← match ← state1.tryMotivatedApply (state1.get! 0) (recursor := recursor) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let [mvarMotive, mvarMajor, mvarInduct, mvarConduit] := state2.goals |
fail "Incorrect number of goals"
let .num _ major := mvarMajor.name | fail "Incorrect form of mvar id"
addTest $ LSpec.check s!"mapply {recursor}" ((← state2.serializeGoals (options := ← read)).map (·.devolatilizeVars) =
#[
buildNamedGoal mvarMotive.name.toString [("n", "Nat")] "Nat → Prop" (.some "motive"),
buildNamedGoal mvarMajor.name.toString [("n", "Nat")] "Nat",
buildNamedGoal mvarInduct.name.toString [("n", "Nat")] "∀ (t : Nat), Nat.below t → ?motive t",
buildNamedGoal mvarConduit.name.toString [("n", "Nat")] s!"?motive ?m.{major} = (n + 0 = n)" (.some "conduit")
])
let tactic := "intro x"
let state3m ← match ← state2.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state3m.serializeGoals (options := ← read)).map (·.devolatilize) =
#[buildGoal [("n", "Nat"), ("x", "Nat")] "Prop" (.some "motive")])
let tactic := "apply Eq"
let state3m2 ← match ← state3m.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let [eqL, eqR, eqT] := state3m2.goals | fail "Incorrect number of goals"
let [_motive, _major, _step, conduit] := state2.goals | panic! "Goals conflict"
let state2b ← match state3m2.resume [conduit] with
| .ok state => pure state
| .error e => do
addTest $ assertUnreachable e
return ()
let cNatAdd := "(:c HAdd.hAdd) (:c Nat) (:c Nat) (:c Nat) ((:c instHAdd) (:c Nat) (:c instAddNat))"
let cNat0 := "((:c OfNat.ofNat) (:c Nat) (:lit 0) ((:c instOfNatNat) (:lit 0)))"
let fvN ← state2b.withContext conduit do
let lctx ← getLCtx
pure $ lctx.getFVarIds.get! 0 |>.name
let conduitRight := s!"((:c Eq) (:c Nat) ({cNatAdd} (:fv {fvN}) {cNat0}) (:fv {fvN}))"
let substOf (mvarId: MVarId) := s!"(:subst (:mv {mvarId.name}) (:fv {fvN}) (:mv {mvarMajor}))"
let .num _ nL := eqL.name | fail "Incorrect form of mvar id"
let .num _ nR := eqR.name | fail "Incorrect form of mvar id"
let nL' := nL + 4
let nR' := nR + 5
addTest $ LSpec.check "resume" ((← state2b.serializeGoals (options := { ← read with printExprAST := true })) =
#[
{
name := mvarConduit.name.toString,
userName? := .some "conduit",
target := {
pp? := .some s!"(?m.{nL'} ?m.{major} = ?m.{nR'} ?m.{major}) = (n + 0 = n)",
sexp? := .some s!"((:c Eq) (:sort 0) ((:c Eq) {substOf eqT} {substOf eqL} {substOf eqR}) {conduitRight})",
},
vars := #[{
name := fvN.toString,
userName := "n",
type? := .some { pp? := .some "Nat", sexp? := .some "(:c Nat)" },
}],
}
])
def test_tactic_failure_unresolved_goals : TestM Unit := do
let state? ← startProof (.expr "∀ (p : Nat → Prop), ∃ (x : Nat), p (0 + x + 0)")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intro p"
let state1 ← match ← state0.tacticOn 0 tactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let tactic := "exact ⟨0, by simp⟩"
let .failure messages ← state1.tacticOn 0 tactic | addTest $ assertUnreachable s!"{tactic} should fail"
checkEq s!"{tactic} fails" messages #[s!"{← getFileName}:0:12: error: unsolved goals\np : Nat → Prop\n⊢ p 0\n"]
def test_tactic_failure_synthesize_placeholder : TestM Unit := do
let state? ← startProof (.expr "∀ (p q r : Prop) (h : p → q), q ∧ r")
let state0 ← match state? with
| .some state => pure state
| .none => do
addTest $ assertUnreachable "Goal could not parse"
return ()
let tactic := "intro p q r h"
let state1 ← match ← state0.tacticOn 0 tactic with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let iex : InternalExceptionId := { idx := 4 }
IO.println s!"{← iex.getName}"
let tactic := "simpa [h] using And.imp_left h _"
--let state2 ← match ← state1.tacticOn 0 tactic with
-- | .success state => pure state
-- | other => do
-- addTest $ assertUnreachable $ other.toString
-- return ()
-- Volatile behaviour. This easily changes across Lean versions
--checkEq tactic ((← state2.serializeGoals).map (·.devolatilize)) #[
-- buildGoal [("p", "Prop"), ("q", "Prop"), ("r", "Prop"), ("h", "p → q")] "p ∧ r"
--]
let .failure messages ← state1.tacticOn 0 tactic | addTest $ assertUnreachable s!"{tactic} should fail"
let message := s!"<Pantograph>:0:31: error: don't know how to synthesize placeholder\ncontext:\np q r : Prop\nh : p → q\n⊢ p ∧ r\n"
checkEq s!"{tactic} fails" messages #[message]
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
let tests := [
("identity", test_identity),
("Nat.add_comm", test_nat_add_comm false),
("Nat.add_comm manual", test_nat_add_comm true),
("Nat.add_comm delta", test_delta_variable),
("arithmetic", test_arith),
("Or.comm", test_or_comm),
("conv", test_conv),
("calc", test_calc),
("Nat.zero_add", test_nat_zero_add),
("Nat.zero_add alt", test_nat_zero_add_alt),
("tactic failure with unresolved goals", test_tactic_failure_unresolved_goals),
("tactic failure with synthesize placeholder", test_tactic_failure_synthesize_placeholder),
]
tests.map (fun (name, test) => (name, proofRunner env test))
end Pantograph.Test.Proofs

109
Test/Serial.lean Normal file
View File

@ -0,0 +1,109 @@
import LSpec
import Test.Common
import Lean
import Pantograph.Library
open Lean
namespace Pantograph.Test.Serial
def tempPath : IO System.FilePath := do
Prod.snd <$> IO.FS.createTempFile
structure MultiState where
coreContext : Core.Context
env: Environment
abbrev TestM := TestT $ StateRefT MultiState $ IO
instance : MonadEnv TestM where
getEnv := return (← getThe MultiState).env
modifyEnv f := do modifyThe MultiState fun s => { s with env := f s.env }
def runCoreM { α } (state : Core.State) (testCoreM : TestT CoreM α) : TestM (α × Core.State) := do
let multiState ← getThe MultiState
let coreM := runTestWithResult testCoreM
match ← (coreM.run multiState.coreContext state).toBaseIO with
| .error e => do
throw $ .userError $ ← e.toMessageData.toString
| .ok ((a, tests), state') => do
set $ (← getThe LSpec.TestSeq) ++ tests
return (a, state')
def test_environment_pickling : TestM Unit := do
let coreSrc : Core.State := { env := ← getEnv }
let coreDst : Core.State := { env := ← getEnv }
let name := `mystery
let envPicklePath ← tempPath
let ((), _) ← runCoreM coreSrc do
let type: Expr := .forallE `p (.sort 0) (.forallE `h (.bvar 0) (.bvar 1) .default) .default
let value: Expr := .lam `p (.sort 0) (.lam `h (.bvar 0) (.bvar 0) .default) .default
let c := Lean.Declaration.defnDecl <| Lean.mkDefinitionValEx
(name := name)
(levelParams := [])
(type := type)
(value := value)
(hints := Lean.mkReducibilityHintsRegularEx 1)
(safety := Lean.DefinitionSafety.safe)
(all := [])
let env' ← match (← getEnv).addDecl (← getOptions) c with
| .error e => do
let error ← (e.toMessageData (← getOptions)).toString
throwError error
| .ok env' => pure env'
environmentPickle env' envPicklePath
let _ ← runCoreM coreDst do
let (env', _) ← environmentUnpickle envPicklePath
checkTrue s!"Has symbol {name}" (env'.find? name).isSome
let anotherName := `mystery2
checkTrue s!"Doesn't have symbol {anotherName}" (env'.find? anotherName).isNone
IO.FS.removeFile envPicklePath
def test_goal_state_pickling_simple : TestM Unit := do
let coreSrc : Core.State := { env := ← getEnv }
let coreDst : Core.State := { env := ← getEnv }
let statePath ← tempPath
let type: Expr := .forallE `p (.sort 0) (.forallE `h (.bvar 0) (.bvar 1) .default) .default
let stateGenerate : MetaM GoalState := runTermElabMInMeta do
GoalState.create type
let ((), _) ← runCoreM coreSrc do
let state ← stateGenerate.run'
goalStatePickle state statePath
let ((), _) ← runCoreM coreDst do
let (goalState, _) ← goalStateUnpickle statePath (← getEnv)
let metaM : MetaM (List Expr) := do
goalState.goals.mapM λ goal => goalState.withContext goal goal.getType
let types ← metaM.run'
checkTrue "Goals" $ types[0]!.equal type
IO.FS.removeFile statePath
structure Test where
name : String
routine: TestM Unit
protected def Test.run (test: Test) (env: Lean.Environment) : IO LSpec.TestSeq := do
-- Create the state
let state : MultiState := {
coreContext := ← createCoreContext #[],
env,
}
match ← ((runTest $ test.routine).run' state).toBaseIO with
| .ok e => return e
| .error e =>
return LSpec.check s!"Emitted exception: {e.toString}" (e.toString == "")
def suite (env : Lean.Environment): List (String × IO LSpec.TestSeq) :=
let tests: List Test := [
{ name := "environment_pickling", routine := test_environment_pickling, },
{ name := "goal_state_pickling_simple", routine := test_goal_state_pickling_simple, },
]
tests.map (fun test => (test.name, test.run env))
end Pantograph.Test.Serial

5
Test/Tactic.lean Normal file
View File

@ -0,0 +1,5 @@
import Test.Tactic.Assign
import Test.Tactic.Congruence
import Test.Tactic.MotivatedApply
import Test.Tactic.NoConfuse
import Test.Tactic.Prograde

33
Test/Tactic/Assign.lean Normal file
View File

@ -0,0 +1,33 @@
import Lean.Meta
import Lean.Elab
import LSpec
import Test.Common
open Lean
namespace Pantograph.Test.Tactic.Assign
def test_draft : TestT Elab.TermElabM Unit := do
let expr := "forall (p : Prop), (p p) p"
let skeleton := "by\nhave a : p p := sorry\nsorry"
let expr ← parseSentence expr
Meta.forallTelescope expr $ λ _ body => do
let skeleton' ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := skeleton)
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
-- Apply the tactic
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let tactic := Tactic.evalDraft skeleton'
let newGoals ← runTacticOnMVar tactic target.mvarId!
addTest $ LSpec.check "goals" ((← newGoals.mapM (λ g => do exprToStr (← g.getType))) = ["p p", "(p p) p"])
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[
("draft", test_draft),
] |>.map (λ (name, t) => (name, runTestTermElabM env t))
end Pantograph.Test.Tactic.Assign

View File

@ -0,0 +1,88 @@
import LSpec
import Lean
import Test.Common
open Lean
open Pantograph
namespace Pantograph.Test.Tactic.Congruence
def test_congr_arg_list : TestT Elab.TermElabM Unit := do
let expr := "λ {α} (l1 l2 : List α) (h: l1 = l2) => l1.reverse = l2.reverse"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let newGoals ← runTacticOnMVar Tactic.evalCongruenceArg target.mvarId!
addTest $ LSpec.check "goals" ((← newGoals.mapM (λ x => mvarUserNameAndType x)) =
[
(`α, "Sort ?u.30"),
(`a₁, "?α"),
(`a₂, "?α"),
(`f, "?α → List α"),
(`h, "?a₁ = ?a₂"),
(`conduit, "(?f ?a₁ = ?f ?a₂) = (l1.reverse = l2.reverse)"),
])
let f := newGoals.get! 3
let h := newGoals.get! 4
let c := newGoals.get! 5
let results ← Meta.withAssignableSyntheticOpaque do f.apply (← parseSentence "List.reverse")
addTest $ LSpec.check "apply" (results.length = 0)
addTest $ LSpec.check "h" ((← exprToStr $ ← h.getType) = "?a₁ = ?a₂")
addTest $ LSpec.check "conduit" ((← exprToStr $ ← c.getType) = "(List.reverse ?a₁ = List.reverse ?a₂) = (l1.reverse = l2.reverse)")
def test_congr_arg : TestT Elab.TermElabM Unit := do
let expr := "λ (n m: Nat) (h: n = m) => n * n = m * m"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let newGoals ← runTacticOnMVar Tactic.evalCongruenceArg target.mvarId!
addTest $ LSpec.check "goals" ((← newGoals.mapM (λ x => mvarUserNameAndType x)) =
[
(`α, "Sort ?u.73"),
(`a₁, "?α"),
(`a₂, "?α"),
(`f, "?α → Nat"),
(`h, "?a₁ = ?a₂"),
(`conduit, "(?f ?a₁ = ?f ?a₂) = (n * n = m * m)"),
])
def test_congr_fun : TestT Elab.TermElabM Unit := do
let expr := "λ (n m: Nat) => (n + m) + (n + m) = (n + m) * 2"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let newGoals ← runTacticOnMVar Tactic.evalCongruenceFun target.mvarId!
addTest $ LSpec.check "goals" ((← newGoals.mapM (λ x => mvarUserNameAndType x)) =
[
(`α, "Sort ?u.165"),
(`f₁, "?α → Nat"),
(`f₂, "?α → Nat"),
(`h, "?f₁ = ?f₂"),
(`a, "?α"),
(`conduit, "(?f₁ ?a = ?f₂ ?a) = (n + m + (n + m) = (n + m) * 2)"),
])
def test_congr : TestT Elab.TermElabM Unit := do
let expr := "λ (a b: Nat) => a = b"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let newGoals ← runTacticOnMVar Tactic.evalCongruence target.mvarId!
addTest $ LSpec.check "goals" ((← newGoals.mapM (λ x => mvarUserNameAndType x)) =
[
(`α, "Sort ?u.10"),
(`f₁, "?α → Nat"),
(`f₂, "?α → Nat"),
(`a₁, "?α"),
(`a₂, "?α"),
(`h₁, "?f₁ = ?f₂"),
(`h₂, "?a₁ = ?a₂"),
(`conduit, "(?f₁ ?a₁ = ?f₂ ?a₂) = (a = b)"),
])
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[
("congrArg List.reverse", test_congr_arg_list),
("congrArg", test_congr_arg),
("congrFun", test_congr_fun),
("congr", test_congr),
] |>.map (λ (name, t) => (name, runTestTermElabM env t))
end Pantograph.Test.Tactic.Congruence

View File

@ -0,0 +1,113 @@
import LSpec
import Lean
import Test.Common
open Lean
open Pantograph
namespace Pantograph.Test.Tactic.MotivatedApply
def test_type_extract : TestT Elab.TermElabM Unit := do
let recursor ← parseSentence "@Nat.brecOn"
let recursorType ← Meta.inferType recursor
addTest $ LSpec.check "recursorType" ("{motive : Nat → Sort ?u.1} → (t : Nat) → ((t : Nat) → Nat.below t → motive t) → motive t" =
(← exprToStr recursorType))
let info ← match Tactic.getRecursorInformation recursorType with
| .some info => pure info
| .none => throwError "Failed to extract recursor info"
addTest $ LSpec.check "iMotive" (info.iMotive = 2)
let motiveType := info.getMotiveType
addTest $ LSpec.check "motiveType" ("Nat → Sort ?u.1" =
(← exprToStr motiveType))
def test_nat_brec_on : TestT Elab.TermElabM Unit := do
let expr := "λ (n t: Nat) => n + 0 = n"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let recursor ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := "@Nat.brecOn")
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
-- Apply the tactic
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let tactic := Tactic.evalMotivatedApply recursor
let newGoals ← runTacticOnMVar tactic target.mvarId!
let test := LSpec.check "goals" ((← newGoals.mapM (λ g => do exprToStr (← g.getType))) =
[
"Nat → Prop",
"Nat",
"∀ (t : Nat), Nat.below t → ?motive t",
"?motive ?m.74 = (n + 0 = n)",
])
addTest test
def test_list_brec_on : TestT Elab.TermElabM Unit := do
let expr := "λ {α : Type} (l: List α) => l ++ [] = [] ++ l"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let recursor ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := "@List.brecOn")
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
-- Apply the tactic
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let tactic := Tactic.evalMotivatedApply recursor
let newGoals ← runTacticOnMVar tactic target.mvarId!
addTest $ LSpec.check "goals" ((← newGoals.mapM (λ g => do exprToStr (← g.getType))) =
[
"Type ?u.90",
"List ?m.92 → Prop",
"List ?m.92",
"∀ (t : List ?m.92), List.below t → ?motive t",
"?motive ?m.94 = (l ++ [] = [] ++ l)",
])
def test_partial_motive_instantiation : TestT Elab.TermElabM Unit := do
let expr := "λ (n t: Nat) => n + 0 = n"
let recursor ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := "@Nat.brecOn")
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
-- Apply the tactic
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let tactic := Tactic.evalMotivatedApply recursor
let newGoals ← runTacticOnMVar tactic target.mvarId!
let majorId := 74
addTest $ (LSpec.check "goals" ((← newGoals.mapM (λ g => do exprToStr (← g.getType))) =
[
"Nat → Prop",
"Nat",
"∀ (t : Nat), Nat.below t → ?motive t",
s!"?motive ?m.{majorId} = (n + 0 = n)",
]))
let [motive, major, step, conduit] := newGoals | panic! "Incorrect goal number"
addTest $ (LSpec.check "goal name" (major.name.toString = s!"_uniq.{majorId}"))
-- Assign motive to `λ x => x + _`
let motive_assign ← parseSentence "λ (x: Nat) => @Nat.add x + 0 = _"
motive.assign motive_assign
addTest $ ← conduit.withContext do
let t := toString (← Meta.ppExpr $ ← conduit.getType)
return LSpec.check "conduit" (t = s!"(Nat.add ?m.{majorId} + 0 = ?m.149 ?m.{majorId}) = (n + 0 = n)")
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[
("type_extract", test_type_extract),
("Nat.brecOn", test_nat_brec_on),
("List.brecOn", test_list_brec_on),
("Nat.brecOn partial motive instantiation", test_partial_motive_instantiation),
] |>.map (λ (name, t) => (name, runTestTermElabM env t))
end Pantograph.Test.Tactic.MotivatedApply

View File

@ -0,0 +1,72 @@
import LSpec
import Lean
import Test.Common
open Lean
open Pantograph
namespace Pantograph.Test.Tactic.NoConfuse
def test_nat : TestT Elab.TermElabM Unit := do
let expr := "λ (n: Nat) (h: 0 = n + 1) => False"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let recursor ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := "h")
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
-- Apply the tactic
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let tactic := Tactic.evalNoConfuse recursor
let newGoals ← runTacticOnMVar tactic target.mvarId!
addTest $ LSpec.check "goals" ((← newGoals.mapM (λ g => do exprToStr (← g.getType))) = [])
def test_nat_fail : TestT Elab.TermElabM Unit := do
let expr := "λ (n: Nat) (h: n = n) => False"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let recursor ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := "h")
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
-- Apply the tactic
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
try
let tactic := Tactic.evalNoConfuse recursor
let _ ← runTacticOnMVar tactic target.mvarId!
addTest $ assertUnreachable "Tactic should fail"
catch _ =>
addTest $ LSpec.check "Tactic should fail" true
def test_list : TestT Elab.TermElabM Unit := do
let expr := "λ (l: List Nat) (h: [] = 1 :: l) => False"
let expr ← parseSentence expr
Meta.lambdaTelescope expr $ λ _ body => do
let recursor ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := "h")
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
-- Apply the tactic
let target ← Meta.mkFreshExprSyntheticOpaqueMVar body
let tactic := Tactic.evalNoConfuse recursor
let newGoals ← runTacticOnMVar tactic target.mvarId!
addTest $ LSpec.check "goals"
((← newGoals.mapM (λ g => do exprToStr (← g.getType))) = [])
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[
("Nat", test_nat),
("Nat fail", test_nat_fail),
("List", test_list),
] |>.map (λ (name, t) => (name, runTestTermElabM env t))
end Pantograph.Test.Tactic.NoConfuse

300
Test/Tactic/Prograde.lean Normal file
View File

@ -0,0 +1,300 @@
import LSpec
import Lean
import Test.Common
open Lean
open Pantograph
namespace Pantograph.Test.Tactic.Prograde
def test_define : TestT Elab.TermElabM Unit := do
let expr := "forall (p q : Prop) (h: p), And (Or p q) (Or p q)"
let expr ← parseSentence expr
Meta.forallTelescope expr $ λ _ body => do
let e ← match Parser.runParserCategory
(env := ← MonadEnv.getEnv)
(catName := `term)
(input := "Or.inl h")
(fileName := ← getFileName) with
| .ok syn => pure syn
| .error error => throwError "Failed to parse: {error}"
-- Apply the tactic
let goal ← Meta.mkFreshExprSyntheticOpaqueMVar body
let target: Expr := mkAnd
(mkOr (.fvar ⟨uniq 8⟩) (.fvar ⟨uniq 9⟩))
(mkOr (.fvar ⟨uniq 8⟩) (.fvar ⟨uniq 9⟩))
let h := .fvar ⟨uniq 8⟩
addTest $ LSpec.test "goals before" ((← toCondensedGoal goal.mvarId!).devolatilize == {
context := #[
cdeclOf `p (.sort 0),
cdeclOf `q (.sort 0),
cdeclOf `h h
],
target,
})
let tactic := Tactic.evalDefine `h2 e
let m := .mvar ⟨uniq 13⟩
let [newGoal] ← runTacticOnMVar tactic goal.mvarId! | panic! "Incorrect goal number"
addTest $ LSpec.test "goals after" ((← toCondensedGoal newGoal).devolatilize == {
context := #[
cdeclOf `p (.sort 0),
cdeclOf `q (.sort 0),
cdeclOf `h h,
{
userName := `h2,
type := mkOr h m,
value? := .some $ mkApp3 (mkConst `Or.inl) h m (.fvar ⟨uniq 10⟩)
}
],
target,
})
let .some e ← getExprMVarAssignment? goal.mvarId! | panic! "Tactic must assign"
addTest $ LSpec.test "assign" e.isLet
def test_define_proof : TestT Elab.TermElabM Unit := do
let rootExpr ← parseSentence "∀ (p q: Prop), p → ((p q) (p q))"
let state0 ← GoalState.create rootExpr
let tactic := "intro p q h"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state1.serializeGoals).map (·.devolatilize) =
#[buildGoal [("p", "Prop"), ("q", "Prop"), ("h", "p")] "(p q) p q"])
let expr := "Or.inl (Or.inl h)"
let state2 ← match ← state1.tryAssign (state1.get! 0) (expr := expr) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!":= {expr}" ((← state2.serializeGoals).map (·.devolatilize) =
#[])
let evalBind := "y"
let evalExpr := "Or.inl h"
let state2 ← match ← state1.tryDefine (state1.get! 0) (binderName := evalBind) (expr := evalExpr) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!"eval {evalBind} := {evalExpr}" ((← state2.serializeGoals).map (·.devolatilize) =
#[{
target := { pp? := .some "(p q) p q"},
vars := #[
{ userName := "p", type? := .some { pp? := .some "Prop" } },
{ userName := "q", type? := .some { pp? := .some "Prop" } },
{ userName := "h", type? := .some { pp? := .some "p" } },
{ userName := "y",
type? := .some { pp? := .some "p ?m.25" },
value? := .some { pp? := .some "Or.inl h" },
}
]
}])
let expr := "Or.inl y"
let state3 ← match ← state2.tryAssign (state2.get! 0) (expr := expr) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!":= {expr}" ((← state3.serializeGoals).map (·.devolatilize) =
#[])
addTest $ LSpec.check "(3 root)" state3.rootExpr?.isSome
def fun_define_root_expr: ∀ (p: Prop), PProd (Nat → p) Unit → p := by
intro p x
apply x.fst
exact 5
def test_define_root_expr : TestT Elab.TermElabM Unit := do
--let rootExpr ← parseSentence "Nat"
--let state0 ← GoalState.create rootExpr
--let .success state1 ← state0.tacticOn (goalId := 0) "exact 5" | addTest $ assertUnreachable "exact 5"
--let .some rootExpr := state1.rootExpr? | addTest $ assertUnreachable "Root expr"
--addTest $ LSpec.check "root" ((toString $ ← Meta.ppExpr rootExpr) = "5")
let rootExpr ← parseSentence "∀ (p: Prop), PProd (Nat → p) Unit → p"
let state0 ← GoalState.create rootExpr
let tactic := "intro p x"
let .success state1 ← state0.tacticOn (goalId := 0) tactic | addTest $ assertUnreachable tactic
let binderName := `binder
let value := "x.fst"
let expr ← state1.goals[0]!.withContext $ strToTermSyntax value
let tacticM := Tactic.evalDefine binderName expr
let .success state2 ← state1.tryTacticM (state1.get! 0) tacticM | addTest $ assertUnreachable s!"define {binderName} := {value}"
let tactic := s!"apply {binderName}"
let .success state3 ← state2.tacticOn (goalId := 0) tactic | addTest $ assertUnreachable tactic
let tactic := s!"exact 5"
let .success state4 ← state3.tacticOn (goalId := 0) tactic | addTest $ assertUnreachable tactic
let .some rootExpr := state4.rootExpr? | addTest $ assertUnreachable "Root expr"
addTest $ LSpec.check "root" ((toString $ ← Meta.ppExpr rootExpr) = "fun p x =>\n let binder := x.fst;\n binder 5")
--set_option pp.all true
--#check @PSigma (α := Prop) (β := λ (p: Prop) => p)
--def test_define_root_expr : TestT Elab.TermElabM Unit := do
def test_have_proof : TestT Elab.TermElabM Unit := do
let rootExpr ← parseSentence "∀ (p q: Prop), p → ((p q) (p q))"
let state0 ← GoalState.create rootExpr
let tactic := "intro p q h"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state1.serializeGoals).map (·.devolatilize) =
#[buildGoal [("p", "Prop"), ("q", "Prop"), ("h", "p")] "(p q) p q"])
let expr := "Or.inl (Or.inl h)"
let state2 ← match ← state1.tryAssign (state1.get! 0) (expr := expr) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!":= {expr}" ((← state2.serializeGoals).map (·.devolatilize) =
#[])
let haveBind := "y"
let haveType := "p q"
let state2 ← match ← state1.tryHave (state1.get! 0) (binderName := haveBind) (type := haveType) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!"have {haveBind}: {haveType}" ((← state2.serializeGoals).map (·.devolatilize) =
#[
buildGoal [("p", "Prop"), ("q", "Prop"), ("h", "p")] "p q",
buildGoal [("p", "Prop"), ("q", "Prop"), ("h", "p"), ("y", "p q")] "(p q) p q"
])
let expr := "Or.inl h"
let state3 ← match ← state2.tryAssign (state2.get! 0) (expr := expr) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!":= {expr}" ((← state3.serializeGoals).map (·.devolatilize) =
#[])
let state2b ← match state3.continue state2 with
| .ok state => pure state
| .error e => do
addTest $ assertUnreachable e
return ()
let expr := "Or.inl y"
let state4 ← match ← state2b.tryAssign (state2b.get! 0) (expr := expr) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check s!":= {expr}" ((← state4.serializeGoals).map (·.devolatilize) =
#[])
let .some rootExpr := state4.rootExpr? | addTest $ assertUnreachable "Root expr"
addTest $ LSpec.check "root" ((toString $ ← Meta.ppExpr rootExpr) = "fun p q h y => Or.inl y")
def test_let (specialized: Bool): TestT Elab.TermElabM Unit := do
let rootExpr ← parseSentence "∀ (p q: Prop), p → ((p q) (p q))"
let state0 ← GoalState.create rootExpr
let tactic := "intro a p h"
let state1 ← match ← state0.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state1.serializeGoals).map (·.devolatilize) =
#[{
target := { pp? := .some mainTarget },
vars := interiorVars,
}])
let letType := "Nat"
let expr := s!"let b: {letType} := _; _"
let result2 ← match specialized with
| true => state1.tryLet (state1.get! 0) (binderName := "b") (type := letType)
| false => state1.tryAssign (state1.get! 0) (expr := expr)
let state2 ← match result2 with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
let serializedState2 ← state2.serializeGoals
let letBindName := if specialized then "b" else "_1"
addTest $ LSpec.check expr (serializedState2.map (·.devolatilize) =
#[{
target := { pp? := .some letType },
vars := interiorVars,
userName? := .some letBindName
},
{
target := { pp? := .some mainTarget },
vars := interiorVars ++ #[{
userName := "b",
type? := .some { pp? := .some letType },
value? := .some { pp? := .some s!"?{letBindName}" },
}],
userName? := if specialized then .none else .some "_2",
}
])
let tactic := "exact 1"
let state3 ← match ← state2.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.check tactic ((← state3.serializeGoals).map (·.devolatilize) = #[])
let state3r ← match state3.continue state2 with
| .error msg => do
addTest $ assertUnreachable $ msg
return ()
| .ok state => pure state
addTest $ LSpec.check "(continue)" ((← state3r.serializeGoals).map (·.devolatilize) =
#[
{
target := { pp? := .some mainTarget },
vars := interiorVars ++ #[{
userName := "b",
type? := .some { pp? := .some "Nat" },
value? := .some { pp? := .some "1" },
}],
userName? := if specialized then .none else .some "_2",
}
])
let tactic := "exact h"
match ← state3r.tacticOn (goalId := 0) (tactic := tactic) with
| .failure #[message] =>
addTest $ LSpec.check tactic (message = s!"type mismatch\n h\nhas type\n a : Prop\nbut is expected to have type\n {mainTarget} : Prop")
| other => do
addTest $ assertUnreachable $ other.toString
let tactic := "exact Or.inl (Or.inl h)"
let state4 ← match ← state3r.tacticOn (goalId := 0) (tactic := tactic) with
| .success state => pure state
| other => do
addTest $ assertUnreachable $ other.toString
return ()
addTest $ LSpec.test "(4 root)" state4.rootExpr?.isSome
where
mainTarget := "(a p) a p"
interiorVars: Array Protocol.Variable := #[
{ userName := "a", type? := .some { pp? := .some "Prop" }, },
{ userName := "p", type? := .some { pp? := .some "Prop" }, },
{ userName := "h", type? := .some { pp? := .some "a" }, }
]
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[
("define", test_define),
("define proof", test_define_proof),
("define root expr", test_define_root_expr),
("have proof", test_have_proof),
("let via assign", test_let false),
("let via tryLet", test_let true),
] |>.map (λ (name, t) => (name, runTestTermElabM env t))
end Pantograph.Test.Tactic.Prograde

View File

@ -1,18 +0,0 @@
#!/usr/bin/env python3
import subprocess, shutil, os, stat
from pathlib import Path
# -- Install Panograph
# Define paths for Pantograph source and Pantograph Python interface
PATH_PANTOGRAPH = Path("./src")
PATH_PY = Path("./pantograph")
with subprocess.Popen(["lake", "build", "repl"], cwd=PATH_PANTOGRAPH) as p:
p.wait()
path_executable = PATH_PY / "pantograph-repl"
shutil.copyfile(PATH_PANTOGRAPH / ".lake/build/bin/repl", path_executable)
os.chmod(path_executable, os.stat(path_executable).st_mode | stat.S_IEXEC)
# -- Copy the Lean toolchain file to the specified path
shutil.copyfile(PATH_PANTOGRAPH / "lean-toolchain", PATH_PY / "lean-toolchain")

157
doc/icon.svg Normal file
View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256"
height="256"
viewBox="0 0 67.733332 67.733333"
version="1.1"
id="svg1"
sodipodi:docname="icon.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showguides="true"
inkscape:zoom="5.1882633"
inkscape:cx="81.819286"
inkscape:cy="132.22151"
inkscape:window-width="3774"
inkscape:window-height="2126"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2">
<sodipodi:guide
position="33.866666,69.8437"
orientation="-1,0"
id="guide1"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="-27.673679,33.866666"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="16.933333,29.94582"
orientation="-1,0"
id="guide3"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="50.799999,37.44627"
orientation="-1,0"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="31.336956,16.933333"
orientation="0,1"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="24.528038,25.4"
orientation="0,1"
id="guide6"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="33.866666,50.799999"
orientation="0,1"
id="guide7"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="32.770414,55.033333"
orientation="0,1"
id="guide8"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="25.347689,33.866666"
orientation="1,0"
id="guide9"
inkscape:locked="false" />
<sodipodi:guide
position="25.347689,42.333333"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="bg" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Circle">
<path
id="path1"
style="fill:#666b98;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.0191989;stroke-miterlimit:3.4;fill-opacity:1"
d="M 33.866666 0.009818522 A 33.857067 33.857067 0 0 0 0.009818522 33.866666 A 33.857067 33.857067 0 0 0 33.866666 67.723514 A 33.857067 33.857067 0 0 0 67.723514 33.866666 A 33.857067 33.857067 0 0 0 33.866666 0.009818522 z M 33.866666 4.2416015 A 29.624933 29.624933 0 0 1 63.491731 33.866666 A 29.624933 29.624933 0 0 1 33.866666 63.491731 A 29.624933 29.624933 0 0 1 4.2416015 33.866666 A 29.624933 29.624933 0 0 1 33.866666 4.2416015 z " />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Pantograph-Core">
<rect
style="fill:#666b98;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.01905;stroke-miterlimit:3.4"
id="rect8"
width="16.942858"
height="4.2257233"
x="33.866665"
y="12.7"
rx="0.58070719"
ry="0.34097314" />
<rect
style="fill:#666b98;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.01905;stroke-miterlimit:3.4"
id="rect1"
width="33.885715"
height="8.4211359"
x="16.933332"
y="42.333332"
rx="0.58070719"
ry="0.34097314" />
<path
style="fill:#666b98;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 42.338095,16.925724 -16.990406,8.474275 13.121218,16.923808 -4.602241,0.0095 -4.254289,0.0015 -8.564029,-16.934789 17.310554,-8.464751 z"
id="path10"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:none;stroke:#666b98;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 46.53445,16.925724 26.018901,26.73418"
id="path11" />
<path
style="fill:none;stroke:#666b98;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 21.048348,25.399999 4.352167,16.934808"
id="path12"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

59
doc/rationale.md Normal file
View File

@ -0,0 +1,59 @@
# Design Rationale
A great problem in machine learning is to use ML agents to automatically prove
mathematical theorems. This sort of proof necessarily involves *search*.
Compatibility for search is the main reason for creating Pantograph. The Lean 4
LSP interface is not conducive to search. Pantograph is designed with this in
mind. It emphasizes the difference between 3 views of a proof:
- **Presentation View**: The view of a written, polished proof. e.g. Mathlib and
math papers are almost always written in this form.
- **Search View**: The view of a proof exploration trajectory. This is not
explicitly supported by Lean LSP.
- **Kernel View**: The proof viewed as a set of metavariables.
Pantograph enables proof agents to operate on the search view.
## Name
The name Pantograph is a pun. It means two things
- A pantograph is an instrument for copying down writing. As an agent explores
the vast proof search space, Pantograph records the current state to ensure
the proof is sound.
- A pantograph is also an equipment for an electric train. It supplies power to
a locomotive. In comparison the (relatively) simple Pantograph software powers
theorem proving projects.
## Caveats and Limitations
Pantograph does not exactly mimic Lean LSP's behaviour. That would not grant the
flexibility it offers. To support tree search means Pantograph has to act
differently from Lean in some times, but never at the sacrifice of soundness.
- When Lean LSP says "don't know how to synthesize placeholder", this indicates
the human operator needs to manually move the cursor to the placeholder and
type in the correct expression. This error therefore should not halt the proof
process, and the placeholder should be turned into a goal.
- When Lean LSP says "unresolved goals", that means a proof cannot finish where
it is supposed to finish at the end of a `by` block. Pantograph will raise the
error in this case, since it indicates the termination of a proof search branch.
- `pick_goal` or `swap` will not work since they run contrary to tree search
paradigms. However, if there are tactics which perform non-trivial operations
to multiple goals at the same time, this constrain could potentially be
relaxed at a cost of great bookkeeping overhead to the user.
Pantograph cannot perform things that are inherently constrained by Lean. These
include:
- If a tactic loses track of metavariables, it will not be caught until the end
of the proof search. This is a bug in the tactic itself.
- Timeouts for executing tactics is not available. Maybe this will change in the
future.
- Interceptions of parsing errors generally cannot be turned into goals (e.g.
`def mystery : Nat := :=`) due to Lean's parsing system.
## References
* [Pantograph Paper](https://arxiv.org/abs/2410.16429)

68
doc/repl.md Normal file
View File

@ -0,0 +1,68 @@
# REPL
## Commands
See `Pantograph/Protocol.lean` for a description of the parameters and return values in JSON.
* `reset`: Delete all cached expressions and proof trees
* `stat`: Display resource usage
* `expr.echo {"expr": <expr>, "type": <optional expected type>, ["levels": [<levels>]]}`: Determine the
type of an expression and format it.
* `env.catalog`: Display a list of all safe Lean symbols in the current environment
* `env.inspect {"name": <name>, "value": <bool>}`: Show the type and package of a
given symbol; If value flag is set, the value is printed or hidden. By default
only the values of definitions are printed.
* `env.save { "path": <fileName> }`, `env.load { "path": <fileName> }`: Save/Load the
current environment to/from a file
* `options.set { key: value, ... }`: Set one or more options (not Lean options; those
have to be set via command line arguments.), for options, see `Pantograph/Protocol.lean`
One particular option for interest for machine learning researchers is the
automatic mode (flag: `"automaticMode"`). By default it is turned on, with
all goals automatically resuming. This makes Pantograph act like a gym,
with no resumption necessary to manage your goals.
* `options.print`: Display the current set of options
* `goal.start {["name": <name>], ["expr": <expr>], ["levels": [<levels>]], ["copyFrom": <symbol>]}`:
Start a new proof from a given expression or symbol
* `goal.tactic {"stateId": <id>, "goalId": <id>, ...}`: Execute a tactic string on a
given goal. The tactic is supplied as additional key-value pairs in one of the following formats:
- `{ "tactic": <tactic> }`: Execute an ordinary tactic
- `{ "expr": <expr> }`: Assign the given proof term to the current goal
- `{ "have": <expr>, "binderName": <name> }`: Execute `have` and creates a branch goal
- `{ "calc": <expr> }`: Execute one step of a `calc` tactic. Each step must
be of the form `lhs op rhs`. An `lhs` of `_` indicates that it should be set
to the previous `rhs`.
- `{ "conv": <bool> }`: Enter or exit conversion tactic mode. In the case of
exit, the goal id is ignored.
- `{ "draft": <expr> }`: Draft an expression with `sorry`s, turning them into goals. Coupling is not allowed.
* `goal.continue {"stateId": <id>, ["branch": <id>], ["goals": <names>]}`:
Execute continuation/resumption
- `{ "branch": <id> }`: Continue on branch state. The current state must have no goals.
- `{ "goals": <names> }`: Resume the given goals
* `goal.remove {"stateIds": [<id>]}"`: Drop the goal states specified in the list
* `goal.print {"stateId": <id>}"`: Print a goal state
* `goal.save { "id": <id>, "path": <fileName> }`, `goal.load { "path": <fileName> }`:
Save/Load a goal state to/from a file. The environment is not carried with the
state. The user is responsible to ensure the sender/receiver instances share
the same environment.
* `frontend.process { ["fileName": <fileName>,] ["file": <str>], invocations:
<bool>, sorrys: <bool>, typeErrorsAsGoals: <bool>, newConstants: <bool> }`:
Executes the Lean frontend on a file, collecting the tactic invocations
(`"invocations": true`), the sorrys and type errors into goal states
(`"sorrys": true`), and new constants (`"newConstants": true`). In the case of
`sorrys`, this command additionally outputs the position of each captured
`sorry`.
## Errors
When an error pertaining to the execution of a command happens, the returning JSON structure is
``` json
{ "error": "type", "desc": "description" }
```
Common error forms:
* `command`: Indicates malformed command structure which results from either
invalid command or a malformed JSON structure that cannot be fed to an
individual command.
* `index`: Indicates an invariant maintained by the output of one command and
input of another is broken. For example, attempting to query a symbol not
existing in the library or indexing into a non-existent proof state.

1
docs/.gitignore vendored
View File

@ -1 +0,0 @@
/_build

View File

@ -1,71 +0,0 @@
# Book settings
# Learn more at https://jupyterbook.org/customize/config.html
# Comprehensive example: https://github.com/executablebooks/jupyter-book/blob/master/docs/_config.yml
title: PyPantograph
author: Leni Aniva
#logo: logo.png
# Force re-execution of notebooks on each build.
# See https://jupyterbook.org/content/execute.html
execute:
execute_notebooks: 'off'
# Define the name of the latex output file for PDF builds
latex:
latex_documents:
targetname: book.tex
# Add a bibtex file so that we can create citations
#bibtex_bibfiles:
# - references.bib
# Information about where the book exists on the web
repository:
url: https://github.com/lenianiva/PyPantograph # Online location of your book
path_to_book: docs # Optional path to your book, relative to the repository root
branch: main # Which branch of the repository should be used when creating links (optional)
# Add GitHub buttons to your book
# See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository
html:
use_issues_button: true
use_repository_button: true
sphinx:
config:
intersphinx_mapping:
ebp:
- "https://executablebooks.org/en/latest/"
- null
myst-parser:
- "https://myst-parser.readthedocs.io/en/latest/"
- null
myst-nb:
- "https://myst-nb.readthedocs.io/en/latest/"
- null
sphinx:
- "https://www.sphinx-doc.org/en/master"
- null
nbformat:
- "https://nbformat.readthedocs.io/en/latest"
- null
sd:
- "https://sphinx-design.readthedocs.io/en/latest"
- null
sphinxproof:
- "https://sphinx-proof.readthedocs.io/en/latest/"
- null
hoverxref_intersphinx:
- "sphinxproof"
mathjax3_config:
tex:
macros:
"N": "\\mathbb{N}"
"floor": ["\\lfloor#1\\rfloor", 1]
"bmat": ["\\left[\\begin{array}"]
"emat": ["\\end{array}\\right]"]
extra_extensions:
- sphinx.ext.intersphinx
- sphinx.ext.autodoc

View File

@ -1,16 +0,0 @@
format: jb-book
root: intro
parts:
- caption: Features
chapters:
- file: setup
- file: goal
- file: agent-search
- file: data
- file: drafting
- caption: API Documentation
chapters:
- file: api-server
- file: api-search
- file: api-expr
- file: api-data

View File

@ -1,155 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ec3abb52-d7cd-471f-b3b7-2d9681c79360",
"metadata": {},
"source": [
"# Search\n",
"\n",
"Pantograph supports basic proof search. In this case, Pantograph treats goals as nodes on an and-or tree. The user supplies an agent which should provide two functions:\n",
"\n",
"1. *Tactic*: Which tactic should be used on a goal?\n",
"2. *Guidance*: What is the search priority on a goal?\n",
"\n",
"The user agent should inherit from `pantograph.search.Agent`. Here is a brute force agent example:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "959458f5-02e4-4f73-ae28-16a756aebed9",
"metadata": {},
"outputs": [],
"source": [
"from typing import Optional\n",
"import collections\n",
"from pantograph import Server\n",
"from pantograph.search import Agent\n",
"from pantograph.expr import GoalState, Tactic"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "8b402602-3ae5-43e4-9a62-2fa9e2c039fa",
"metadata": {},
"outputs": [],
"source": [
"class DumbAgent(Agent):\n",
"\n",
" def __init__(self):\n",
" super().__init__()\n",
"\n",
" self.goal_tactic_id_map = collections.defaultdict(lambda : 0)\n",
" self.intros = [\n",
" \"intro\",\n",
" ]\n",
" self.tactics = [\n",
" \"intro h\",\n",
" \"cases h\",\n",
" \"apply Or.inl\",\n",
" \"apply Or.inr\",\n",
" ]\n",
" self.no_space_tactics = [\n",
" \"assumption\",\n",
" ]\n",
"\n",
" def next_tactic(\n",
" self,\n",
" state: GoalState,\n",
" goal_id: int,\n",
" ) -> Optional[Tactic]:\n",
" key = (state.state_id, goal_id)\n",
" i = self.goal_tactic_id_map[key]\n",
"\n",
" target = state.goals[goal_id].target\n",
" if target.startswith('∀'):\n",
" tactics = self.intros\n",
" elif ' ' in target:\n",
" tactics = self.tactics\n",
" else:\n",
" tactics = self.no_space_tactics\n",
"\n",
" if i >= len(tactics):\n",
" return None\n",
"\n",
" self.goal_tactic_id_map[key] = i + 1\n",
" return tactics[i]"
]
},
{
"cell_type": "markdown",
"id": "665db9d0-5fff-4b26-9cea-32d06a6e1e04",
"metadata": {},
"source": [
"Execute the search with `agent.search`."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "1c7961d1-b1fa-498c-ab75-16feb784ca2c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"SearchResult(n_goals_root=1, duration=0.7717759609222412, success=True, steps=16)"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"server = Server()\n",
"agent = DumbAgent()\n",
"goal_state = server.goal_start(\"∀ (p q: Prop), Or p q -> Or q p\")\n",
"agent.search(server=server, goal_state=goal_state, verbose=False)"
]
},
{
"cell_type": "markdown",
"id": "141e0116-cbb6-4957-aaea-2a1100f80ece",
"metadata": {},
"source": [
"## Automatic and Manual Modes\n",
"\n",
"The agent chooses one goal and executes a tactic on this goal. What happens to the other goals that are not chosen? By default, the server runs in automatic mode. In automatic mode, all other goals are automatically inherited by a child state, so a user agent could declare a proof finished when there are no more goals remaining in the current goal state.\n",
"\n",
"Some users may wish to handle sibling goals manually. For example, Aesop's treatment of metavariable coupling is not automatic. To do this, pass the flag `options={ \"automaticMode\" : False }` to the `Server` constructor."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2090e538-d196-4923-937c-b83fedf1d9a2",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -1,5 +0,0 @@
Data
=============
.. automodule:: pantograph.data
:members:

View File

@ -1,8 +0,0 @@
Expr
=============
.. automodule:: pantograph.expr
:members:
.. autodata:: pantograph.expr.Expr
.. autodata:: pantograph.expr.Tactic

View File

@ -1,5 +0,0 @@
Search
=============
.. automodule:: pantograph.search
:members:

View File

@ -1,5 +0,0 @@
Server
=============
.. automodule:: pantograph.server
:members:

View File

@ -1,244 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "fe7a3037-5c49-4097-9a5d-575b958cc7f8",
"metadata": {},
"source": [
"# Data Extraction"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "fc68ad1d-e64c-48b7-9461-50d872d30473",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from pathlib import Path\n",
"from pantograph.server import Server"
]
},
{
"cell_type": "markdown",
"id": "fd13c644-d731-4f81-964e-584bbd43e51c",
"metadata": {},
"source": [
"## Tactic Invocation\n",
"\n",
"Pantograph can extract tactic invocation data from a Lean file. A **tactic\n",
"invocation** is a tuple containing the before and after goal states, and the\n",
"tactic which converts the \"before\" state to the \"after\" state.\n",
"\n",
"To extract tactic invocation data, use `server.tactic_invocations(file_name)`\n",
"and supply the file name of the input Lean file."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "6282dc6f-4eac-4263-8277-9d54d19ad1a5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"$PWD: /home/aniva/Projects/atp/PyPantograph/examples/Example\n"
]
}
],
"source": [
"project_path = Path(os.getcwd()).parent.resolve() / 'examples/Example'\n",
"print(f\"$PWD: {project_path}\")\n",
"server = Server(imports=['Example'], project_path=project_path)\n",
"units = server.tactic_invocations(project_path / \"Example.lean\")"
]
},
{
"cell_type": "markdown",
"id": "c3c1be91-27a5-4481-b09d-a32dbb94b058",
"metadata": {},
"source": [
"The function returns a list of `CompilationUnit` objects, corresponding to each compilation unit in the input Lean file. For performance reasons only the text boundaries are loaded into `CompilationUnit`s."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e994aa2b-5d5e-4f86-af6c-40e0b3a032d2",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"#0: [14,85]\n",
"/-- Ensure that Aesop is running -/\n",
"example : αα :=\n",
" by aesop\n",
"\n",
"\n",
"#1: [85,254]\n",
"example : ∀ (p q: Prop), p q → q p := by\n",
" intro p q h\n",
" -- Here are some comments\n",
" cases h\n",
" . apply Or.inr\n",
" assumption\n",
" . apply Or.inl\n",
" assumption\n",
"\n"
]
}
],
"source": [
"with open(project_path / \"Example.lean\", 'rb') as f:\n",
" content = f.read()\n",
" for i, unit in enumerate(units):\n",
" print(f\"#{i}: [{unit.i_begin},{unit.i_end}]\")\n",
" unit_text = content[unit.i_begin:unit.i_end].decode('utf-8')\n",
" print(unit_text)"
]
},
{
"cell_type": "markdown",
"id": "52e650fc-4a87-445f-8aa8-707ed9e36c03",
"metadata": {},
"source": [
"Each `CompilationUnit` includes a list of `TacticInvocation`s, which contains the `.before` (corresponding to the state before the tactic), `.after` (corresponding to the state after the tactic), and `.tactic` (tactic executed) fields. "
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "8e0f0def-dd3c-4550-8a7c-b4aec6c7fd7f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[Before]\n",
"α : Sort ?u.7\n",
"⊢ αα\n",
"[Tactic]\n",
"aesop (using [])\n",
"[After]\n",
"\n"
]
}
],
"source": [
"for i in units[0].invocations:\n",
" print(f\"[Before]\\n{i.before}\")\n",
" print(f\"[Tactic]\\n{i.tactic} (using {i.used_constants})\")\n",
" print(f\"[After]\\n{i.after}\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "51f5398b-5416-4dc1-81cd-6d2514758232",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[Before]\n",
"⊢ ∀ (p q : Prop), p q → q p\n",
"[Tactic]\n",
"intro p q h (using [])\n",
"[After]\n",
"p q : Prop\n",
"h : p q\n",
"⊢ q p\n",
"[Before]\n",
"p q : Prop\n",
"h : p q\n",
"⊢ q p\n",
"[Tactic]\n",
"cases h (using ['Eq.refl', 'Or'])\n",
"[After]\n",
"case inl\n",
"p q : Prop\n",
"h✝ : p\n",
"⊢ q p\n",
"case inr p q : Prop h✝ : q ⊢ q p\n",
"[Before]\n",
"case inl\n",
"p q : Prop\n",
"h✝ : p\n",
"⊢ q p\n",
"[Tactic]\n",
"apply Or.inr (using ['Or.inr'])\n",
"[After]\n",
"case inl.h\n",
"p q : Prop\n",
"h✝ : p\n",
"⊢ p\n",
"[Before]\n",
"case inl.h\n",
"p q : Prop\n",
"h✝ : p\n",
"⊢ p\n",
"[Tactic]\n",
"assumption (using [])\n",
"[After]\n",
"\n",
"[Before]\n",
"case inr\n",
"p q : Prop\n",
"h✝ : q\n",
"⊢ q p\n",
"[Tactic]\n",
"apply Or.inl (using ['Or.inl'])\n",
"[After]\n",
"case inr.h\n",
"p q : Prop\n",
"h✝ : q\n",
"⊢ q\n",
"[Before]\n",
"case inr.h\n",
"p q : Prop\n",
"h✝ : q\n",
"⊢ q\n",
"[Tactic]\n",
"assumption (using [])\n",
"[After]\n",
"\n"
]
}
],
"source": [
"for i in units[1].invocations:\n",
" print(f\"[Before]\\n{i.before}\")\n",
" print(f\"[Tactic]\\n{i.tactic} (using {i.used_constants})\")\n",
" print(f\"[After]\\n{i.after}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -1,165 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "aecc5260-56ad-4734-8917-3a4d92910309",
"metadata": {},
"source": [
"# Drafting\n",
"\n",
"Pantograph supports drafting (technically the sketch step) from\n",
"[Draft-Sketch-Prove](https://github.com/wellecks/ntptutorial/tree/main/partII_dsp).\n",
"Pantograph's drafting feature is more powerful. At any place in the proof, you\n",
"can replace an expression with `sorry`, and the `sorry` will become a goal. Any type errors will also become goals. In order to detect whether type errors have occurred, the user can look at the messages from each compilation unit.\n",
"\n",
"At this point we must introduce the idea of compilation units. Each Lean\n",
"definition, theorem, constant, etc., is a *compilation unit*. When Pantograph\n",
"extracts data from Lean source code, it sections the data into these compilation\n",
"units.\n",
"\n",
"For example, consider this sketch produced by a language model prover:\n",
"```lean\n",
"theorem add_comm_proved_formal_sketch : ∀ n m : Nat, n + m = m + n := by\n",
" intros n m\n",
" induction n with\n",
" | zero =>\n",
" have h_base: 0 + m = m := sorry\n",
" have h_symm: m + 0 = m := sorry\n",
" sorry\n",
" | succ n ih =>\n",
" have h_inductive: n + m = m + n := sorry\n",
" have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := sorry\n",
" have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := sorry\n",
" have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := sorry\n",
" sorry\n",
"```\n",
"There are some `sorry`s that we want to solve automatically with hammer tactics. We can do this by drafting. Feeding this into the drafting feature produces one goal state (corresponding to the one compilation unit) containing as many goals as the draft has `sorry`s:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "52bd153d-235c-47fa-917e-415d444867a5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"m : Nat\n",
"⊢ 0 + m = m\n",
"m : Nat\n",
"h_base : 0 + m = m\n",
"⊢ m + 0 = m\n",
"m : Nat\n",
"h_base : 0 + m = m\n",
"h_symm : m + 0 = m\n",
"⊢ 0 + m = m + 0\n",
"m : Nat\n",
"n : Nat\n",
"ih : n + m = m + n\n",
"⊢ n + m = m + n\n",
"m : Nat\n",
"n : Nat\n",
"ih : n + m = m + n\n",
"h_inductive : n + m = m + n\n",
"⊢ m + n.succ = (m + n).succ\n",
"m : Nat\n",
"n : Nat\n",
"ih : n + m = m + n\n",
"h_inductive : n + m = m + n\n",
"h_pull_succ_out_from_right : m + n.succ = (m + n).succ\n",
"⊢ (n + m).succ = (m + n).succ\n",
"m : Nat\n",
"n : Nat\n",
"ih : n + m = m + n\n",
"h_inductive : n + m = m + n\n",
"h_pull_succ_out_from_right : m + n.succ = (m + n).succ\n",
"h_flip_n_plus_m : (n + m).succ = (m + n).succ\n",
"⊢ n.succ + m = (n + m).succ\n",
"m : Nat\n",
"n : Nat\n",
"ih : n + m = m + n\n",
"h_inductive : n + m = m + n\n",
"h_pull_succ_out_from_right : m + n.succ = (m + n).succ\n",
"h_flip_n_plus_m : (n + m).succ = (m + n).succ\n",
"h_pull_succ_out_from_left : n.succ + m = (n + m).succ\n",
"⊢ n + 1 + m = m + (n + 1)\n"
]
}
],
"source": [
"from pantograph import Server\n",
"\n",
"sketch = \"\"\"\n",
"theorem add_comm_proved_formal_sketch : ∀ n m : Nat, n + m = m + n := by\n",
" -- Consider some n and m in Nats.\n",
" intros n m\n",
" -- Perform induction on n.\n",
" induction n with\n",
" | zero =>\n",
" -- Base case: When n = 0, we need to show 0 + m = m + 0.\n",
" -- We have the fact 0 + m = m by the definition of addition.\n",
" have h_base: 0 + m = m := sorry\n",
" -- We also have the fact m + 0 = m by the definition of addition.\n",
" have h_symm: m + 0 = m := sorry\n",
" -- Combine facts to close goal\n",
" sorry\n",
" | succ n ih =>\n",
" -- Inductive step: Assume n + m = m + n, we need to show succ n + m = m + succ n.\n",
" -- By the inductive hypothesis, we have n + m = m + n.\n",
" have h_inductive: n + m = m + n := sorry\n",
" -- 1. Note we start with: Nat.succ n + m = m + Nat.succ n, so, pull the succ out from m + Nat.succ n on the right side from the addition using addition facts Nat.add_succ.\n",
" have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := sorry\n",
" -- 2. then to flip m + S n to something like S (n + m) we need to use the IH.\n",
" have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := sorry\n",
" -- 3. Now the n & m are on the correct sides Nat.succ n + m = Nat.succ (n + m), so let's use the def of addition to pull out the succ from the addition on the left using Nat.succ_add.\n",
" have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := sorry\n",
" -- Combine facts to close goal\n",
" sorry\n",
"\"\"\"\n",
"\n",
"server = Server()\n",
"unit, = server.load_sorry(sketch)\n",
"print(unit.goal_state)"
]
},
{
"cell_type": "markdown",
"id": "0d4dda56-7b7f-4c4c-b59d-af6f857d7788",
"metadata": {},
"source": [
"For an in-depth example, see `experiments/dsp`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eaf8e506-a6d1-4e9a-ad7a-f7bbb82e01c6",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -1,317 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "c5106980-4850-4bea-a333-5a1b2e4d1dc5",
"metadata": {},
"source": [
"# Goals and Tactics\n",
"\n",
"Executing tactics in Pantograph is simple. To start a proof, call the\n",
"`Server.goal_start` function and supply an expression."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3257de2b-41ca-4cfe-b66c-1ef4781c98b0",
"metadata": {},
"outputs": [],
"source": [
"from pantograph import Server\n",
"from pantograph.expr import TacticHave, TacticCalc, TacticExpr"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "6783d478-d8c7-4c4e-a56e-8170384297ef",
"metadata": {},
"outputs": [],
"source": [
"server = Server()\n",
"state0 = server.goal_start(\"forall (p q: Prop), Or p q -> Or q p\")"
]
},
{
"cell_type": "markdown",
"id": "bfe5a9df-33c2-4538-a9ce-fc0e02c92ff2",
"metadata": {},
"source": [
"This creates a *goal state*, which consists of a finite number of goals. In this\n",
"case since it is the beginning of a state, it has only one goal."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "eefc9094-9574-4f92-9aa2-c39beb85389b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"⊢ forall (p q: Prop), Or p q -> Or q p\n"
]
}
],
"source": [
"print(state0)"
]
},
{
"cell_type": "markdown",
"id": "26dbe212-e09e-42dd-ab15-65ee2fba6234",
"metadata": {},
"source": [
"To execute a tactic on a goal state, use `Server.goal_tactic`. This function\n",
"takes a goal id and a tactic. Most Lean tactics are strings."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "c907dbb6-4d6a-4aa7-b173-60220165ba9e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a : Prop\n",
"⊢ ∀ (q : Prop), a q → q a\n"
]
}
],
"source": [
"state1 = server.goal_tactic(state0, goal_id=0, tactic=\"intro a\")\n",
"print(state1)"
]
},
{
"cell_type": "markdown",
"id": "9978fdcf-a12b-4f22-9551-5e04c262e5e0",
"metadata": {},
"source": [
"Executing a tactic produces a new goal state. If this goal state has no goals,\n",
"the proof is complete. You can recover the usual form of a goal with `str()`"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "16595c5e-2285-49d5-8340-397ad1e6c9e7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'a : Prop\\n⊢ ∀ (q : Prop), a q → q a'"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"str(state1.goals[0])"
]
},
{
"cell_type": "markdown",
"id": "67f2a75d-6851-4393-bac9-a091400f1906",
"metadata": {},
"source": [
"## Error Handling and GC\n",
"\n",
"When a tactic fails, it throws an exception:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "c9784ba2-3810-4f80-a6c4-33d5eef3003e",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[\"tactic 'assumption' failed\\na : Prop\\n⊢ ∀ (q : Prop), a q → q a\"]\n"
]
}
],
"source": [
"try:\n",
" state2 = server.goal_tactic(state1, goal_id=0, tactic=\"assumption\")\n",
" print(\"Should not reach this\")\n",
"except Exception as e:\n",
" print(e)"
]
},
{
"cell_type": "markdown",
"id": "1ae60d9e-8656-4f26-b495-d04bced250fc",
"metadata": {},
"source": [
"A state with no goals is considered solved"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "1cb96b19-d3bb-4533-abeb-a7dbc5bc8c3e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"GoalState(state_id=5, goals=[], _sentinel=[0, 1])"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"state0 = server.goal_start(\"forall (p : Prop), p -> p\")\n",
"state1 = server.goal_tactic(state0, goal_id=0, tactic=\"intro\")\n",
"state2 = server.goal_tactic(state1, goal_id=0, tactic=\"intro h\")\n",
"state3 = server.goal_tactic(state2, goal_id=0, tactic=\"exact h\")\n",
"state3"
]
},
{
"cell_type": "markdown",
"id": "a2945e71-e583-4ae0-9c0f-83035f0492f2",
"metadata": {},
"source": [
"Execute `server.gc()` once in a while to delete unused goals."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "d53624ff-c720-4847-98f7-28e109eb76e7",
"metadata": {},
"outputs": [],
"source": [
"server.gc()"
]
},
{
"cell_type": "markdown",
"id": "0b59e05e-7d8c-4fad-b8ca-375ea995ea5b",
"metadata": {},
"source": [
"## Special Tactics\n",
"\n",
"Lean has special provisions for some tactics. This includes `have`, `let`,\n",
"`calc`. To execute one of these tactics, create a `TacticHave`, `TacticLet`,\n",
"`TacticCalc` instance and feed it into `server.goal_tactic`.\n",
"\n",
"Technically speaking `have` and `let` are not tactics in Lean, so their execution requires special attention."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "526d620b-064f-4ec0-a7b2-6a1ef3c6f6e7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"⊢ 2 = 1 + 1\n",
"h : 2 = 1 + 1\n",
"⊢ 1 + 1 = 2\n"
]
}
],
"source": [
"state0 = server.goal_start(\"1 + 1 = 2\")\n",
"state1 = server.goal_tactic(state0, goal_id=0, tactic=TacticHave(branch=\"2 = 1 + 1\", binder_name=\"h\"))\n",
"print(state1)"
]
},
{
"cell_type": "markdown",
"id": "c415d436-ed0d-475f-bf5e-b8dc63954c7e",
"metadata": {},
"source": [
"The `TacticExpr` \"tactic\" parses an expression and assigns it to the current\n",
"goal. This leverages Lean's type unification system and is as expressive as\n",
"Lean expressions. Many proofs in Mathlib4 are written in a mixture of expression\n",
"and tactic forms."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "e1f06441-4d77-45a7-a1c3-b800b96a8105",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
}
],
"source": [
"state0 = server.goal_start(\"forall (p : Prop), p -> p\")\n",
"state1 = server.goal_tactic(state0, goal_id=0, tactic=\"intro p\")\n",
"state2 = server.goal_tactic(state1, goal_id=0, tactic=TacticExpr(\"fun h => h\"))\n",
"print(state2)"
]
},
{
"cell_type": "markdown",
"id": "d6bcd026-507b-4b1c-8dee-006df53636b0",
"metadata": {},
"source": [
"To execute the `conv` tactic, use `server.goal_conv_begin` to enter conv mode on\n",
"one goal, and use `server.goal_conv_end` to exit from conv mode. Pantograph will\n",
"provide interactive feedback upon every tactic executed in `conv` mode."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2b0b27c6-0c69-4255-aed1-c0713c227ccc",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -1,72 +0,0 @@
# Introduction
This is Pantograph, an machine-to-machine interaction interface for Lean 4.
Its main purpose is to train and evaluate theorem proving agents. The main
features are:
1. Interfacing via the Python library, REPL, or C Library
2. A mixture of expression-based and tactic-based proofs
3. Pantograph has been designed with facilitating search in mind. A theorem
proving agent can simultaneously explore multiple branches of the proof.
4. Handling of metavariable coupling
5. Reading/Adding symbols from the environment
6. Extraction of tactic training data
7. Support for drafting
## Design Rationale
The Lean 4 interface is not conducive to search. Readers familiar with Coq may
know that the Coq Serapi was superseded by CoqLSP. In the opinion of the
authors, this is a mistake. An interface conducive for human operators to write
proofs is often not an interface conductive to search.
Almost all of Pantograph's business logic is written in Lean, and Pantograph
achieves tighter coupling between the data extraction and proof search
components.
## Caveats and Limitations
Pantograph does not exactly mimic Lean LSP's behaviour. That would not grant the
flexibility it offers. To support tree search means Pantograph has to act
differently from Lean in some times, but never at the sacrifice of soundness.
- When Lean LSP says "don't know how to synthesize placeholder", this indicates
the human operator needs to manually move the cursor to the placeholder and
type in the correct expression. This error therefore should not halt the proof
process, and the placeholder should be turned into a goal.
- When Lean LSP says "unresolved goals", that means a proof cannot finish where
it is supposed to finish at the end of a `by` block. Pantograph will raise the
error in this case, since it indicates the termination of a proof search branch.
- `pick_goal` or `swap` will not work since they run contrary to tree search
paradigms. However, if there are tactics which perform non-trivial operations
to multiple goals at the same time, this constrain could potentially be
relaxed at a cost of great bookkeeping overhead to the user.
Pantograph cannot perform things that are inherently constrained by Lean. These
include:
- If a tactic loses track of metavariables, it will not be caught until the end
of the proof search. This is a bug in the tactic itself.
- Timeouts for executing tactics is not available. Maybe this will change in the
future.
- Interceptions of parsing errors generally cannot be turned into goals (e.g.
`def mystery : Nat := :=`) due to Lean's parsing system.
Each Pantograph version is anchored to a Lean version specified in
`src/lean-toolchain`. Features can be backported to older Lean versions upon
request.
## Referencing
[Paper Link](https://arxiv.org/abs/2410.16429)
```bib
@misc{pantograph,
title={Pantograph: A Machine-to-Machine Interaction Interface for Advanced Theorem Proving, High Level Reasoning, and Data Extraction in Lean 4},
author={Leni Aniva and Chuyue Sun and Brando Miranda and Clark Barrett and Sanmi Koyejo},
year={2024},
eprint={2410.16429},
archivePrefix={arXiv},
primaryClass={cs.LO},
url={https://arxiv.org/abs/2410.16429},
}
```

View File

@ -1,53 +0,0 @@
# Setup
Install `poetry`. Then, run
```sh
poetry build
```
This builds a wheel of Pantograph in `dist` which can then be installed. For
example, a downstream project could have this line in its `pyproject.toml`
```toml
pantograph = { file = "path/to/wheel/dist/pantograph-0.2.19-cp312-cp312-manylinux_2_40_x86_64.whl" }
```
To run the examples and experiments, setup a poetry shell:
```sh
poetry install
poetry shell
```
This drops the current shell into an environment where the development packages are available.
All interactions with Lean pass through the `Server` class. Create an instance
with
```python
from pantograph import Server
server = Server()
```
## Lean Dependencies
The server created from `Server()` is sufficient for basic theorem proving tasks
reliant on Lean's `Init` library. Some users may find this insufficient and want
to use non-builtin libraries such as Aesop or Mathlib4.
To use external Lean dependencies such as
[Mathlib4](https://github.com/leanprover-community/mathlib4), Pantograph relies
on an existing Lean repository. Instructions for creating this repository can be
found [here](https://docs.lean-lang.org/lean4/doc/setup.html#lake).
After creating this initial Lean repository, execute in the repository
```sh
lake build
```
to build all files from the repository. This step is necessary after any file in
the repository is modified.
Then, feed the repository's path to the server
```python
server = Server(project_path="./path-to-lean-repo/")
```
For a complete example, see `examples/`.

View File

@ -1,3 +0,0 @@
/build
/lakefile.olean
/lake-packages/*

View File

@ -1,14 +0,0 @@
import Aesop
/-- Ensure that Aesop is running -/
example : αα :=
by aesop
example : ∀ (p q: Prop), p q → q p := by
intro p q h
-- Here are some comments
cases h
. apply Or.inr
assumption
. apply Or.inl
assumption

View File

@ -1,25 +0,0 @@
{"version": "1.1.0",
"packagesDir": ".lake/packages",
"packages":
[{"url": "https://github.com/leanprover-community/batteries",
"type": "git",
"subDir": null,
"scope": "",
"rev": "4756e0fc48acce0cc808df0ad149de5973240df6",
"name": "batteries",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
"inherited": true,
"configFile": "lakefile.lean"},
{"url": "https://github.com/leanprover-community/aesop.git",
"type": "git",
"subDir": null,
"scope": "",
"rev": "28fa80508edc97d96ed6342c9a771a67189e0baa",
"name": "aesop",
"manifestFile": "lake-manifest.json",
"inputRev": "v4.12.0",
"inherited": false,
"configFile": "lakefile.toml"}],
"name": "Example",
"lakeDir": ".lake"}

View File

@ -1,10 +0,0 @@
import Lake
open Lake DSL
require aesop from git
"https://github.com/leanprover-community/aesop.git" @ "v4.12.0"
package Example
@[default_target]
lean_lib Example

View File

@ -1 +0,0 @@
../../src/lean-toolchain

View File

@ -1,33 +0,0 @@
# Examples
For a quick introduction of the API, fire up Jupyter and open `all.ipynb`. (Did
you remember to `poetry install`?)
``` sh
poetry run jupyter notebook
```
This example showcases how to bind library dependencies and execute the `Aesop`
tactic in Lean. First build the example project:
``` sh
pushd Example
lake build
popd
```
This would generate compiled `.olean` files. Then run one of the examples from the
project root:
``` sh
poetry run examples/aesop.py
poetry run examples/sketch.py
poetry run examples/data.py
```
Warning: If you make modifications to any Lean files, you must re-run `lake
build`! Moreover, the version of the Lean used in the example folder (including
dependencies in `lakefile.lean` and `lean-toolchain`) **must match exactly**
with the version in `src/`!
* `aesop.py`: Example of how to use the `aesop` tactic
* `data.py`: Example of loading training data
* `sketch.py`: Example of loading a sketch

View File

@ -1,19 +0,0 @@
#!/usr/bin/env python3
import subprocess
from pathlib import Path
from pantograph.server import Server
def get_project_and_lean_path():
cwd = Path(__file__).parent.resolve() / 'Example'
p = subprocess.check_output(['lake', 'env', 'printenv', 'LEAN_PATH'], cwd=cwd)
return cwd, p
if __name__ == '__main__':
project_path, lean_path = get_project_and_lean_path()
print(f"$PWD: {project_path}")
print(f"$LEAN_PATH: {lean_path}")
server = Server(imports=['Example'], project_path=project_path, lean_path=lean_path)
state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
state1 = server.goal_tactic(state0, goal_id=0, tactic="aesop")
assert state1.is_solved

View File

@ -1,36 +0,0 @@
#!/usr/bin/env python3
from pantograph.server import Server
sketch = """
theorem add_comm_proved_formal_sketch : n m : Nat, n + m = m + n := by
-- Consider some n and m in Nats.
intros n m
-- Perform induction on n.
induction n with
| zero =>
-- Base case: When n = 0, we need to show 0 + m = m + 0.
-- We have the fact 0 + m = m by the definition of addition.
have h_base: 0 + m = m := sorry
-- We also have the fact m + 0 = m by the definition of addition.
have h_symm: m + 0 = m := sorry
-- Combine facts to close goal
sorry
| succ n ih =>
-- Inductive step: Assume n + m = m + n, we need to show succ n + m = m + succ n.
-- By the inductive hypothesis, we have n + m = m + n.
have h_inductive: n + m = m + n := sorry
-- 1. Note we start with: Nat.succ n + m = m + Nat.succ n, so, pull the succ out from m + Nat.succ n on the right side from the addition using addition facts Nat.add_succ.
have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := sorry
-- 2. then to flip m + S n to something like S (n + m) we need to use the IH.
have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := sorry
-- 3. Now the n & m are on the correct sides Nat.succ n + m = Nat.succ (n + m), so let's use the def of addition to pull out the succ from the addition on the left using Nat.succ_add.
have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := sorry
-- Combine facts to close goal
sorry
"""
if __name__ == '__main__':
server = Server()
unit, = server.load_sorry(sketch)
print(unit.goal_state)

View File

@ -1 +0,0 @@
/result

View File

@ -1,68 +0,0 @@
# Lean Draft Sketch Prove (DSP)
based on Sean Welleck's DSP for Isabelle: https://github.com/wellecks/ntptutorial/tree/main/partII_dsp
## Execution
First of all, build the experiment repo.
``` sh
# experiments/dsp
cd lean_src_proj
lake build
```
Then run `main.py`
``` sh
python3 main.py -h
```
The main command for running DSP is `eval`. Due to the multitude of data format
out there, use the `--format` flag to specify the data format. For example,
running DSP on minif2f is:
``` sh
python3 main.py eval \
--dataset ../minif2f/valid.jsonl \
--format minif2f \
--output results-minif2f-valid
```
Then, use `plot.py` to generate the plots
``` sh
python3 plot.py \
--result results-minif2f-{valid,test} \
--names valid test \
--plot-output output-plot
```
## Related work
### Tony's AF
Ton'y original AF: ((Yuhuai et al.))[https://arxiv.org/abs/2205.12615]
Tony's paper improve MiniF2F from: `29.6% to 35.2%`, by `5.6%`.
Expert Iteration:
- AF used: "We explore if one can improve neural theorem provers by training the neural models on proofs of automatically translated theorems".
- they only translate **problem theorems** (nl_thm := "problem + answer") then use a prover to get the formal proof.
- ExpIt algorithn:
- `M_0 := Isabelle_Thor()`
- `Search/Prover := Best_First_Search()` # TODO recall best first search
- ExpIT.fine_tune := "train model to predict next proof_step/tactic given current proof_state and previous proof_step on successful proofs.
- i.e., `<x=(proof_state_{t}, proof_step_{t-1}), y=(proof_step_{t})>` #TODO: I think, confirm with Albert https://twitter.com/messages/1253358235-1267913180153548800
Base Model for Neural Theorem Prover (NTP):
- Thor_GPT2 := "We use a pre-trained and fine-tuned Thor based on a GPT-2 with 700M non-embedding parameters." Note: ReProver used 299M parameters enc-dec.
- fine-tuned on the PILE arxiv + github
Neural Theorem Prover (NTP) for `M_0`:
- Thor :=
- The Thor agent is fine-tuned on the PISA dataset which consists of 2.49 million proof steps from the Isabelle/HOL library.
- The model is trained with the objective to predict the next token in va proof step, given the proof state and the last proof step.
- proof step := "tactic in Isabelle" #TODO confirm with Albert https://twitter.com/messages/1253358235-1267913180153548800
Questions:
- Q1: what is this: "we perform deduplication by problem statements" when does it matter? All MATH train are unique, so why would I care about this?
Idea:
- Idea1: use the formal ground truth solution string in MATH, implement Draft Sketch Proof (DSP) for Lean4 + use some symbolic/ntp solver (hammer/tidy/ReProver)

View File

@ -1,3 +0,0 @@
{"problem": "$ E = \\left[\\begin{array}{rr}5 & 1 \\\\ 2 & 3\\end{array}\\right]$ What is the determinant of $ E$ ?", "hints": ["The determinant of a 2x2 matrix can be computed the following way:", "$ = $", "In this specific case,", "$ = $", "$ = 13 $"]}
{"problem": "If $a + b + c = 9$, what is $7c + 7a + 7b$ ?", "hints": ["$= 7a + 7b + 7c$", "$= (7) \\cdot (a + b + c) $", "$= (7) \\cdot (9) $", "$= 63$"]}
{"problem": "Find $\\lim_{x\\to\\infty}\\dfrac{x^2-4}{\\cos(x)}$. Choose 1 answer: Choose 1 answer: (Choice A) A $4$ (Choice B) B $-2$ (Choice C) C $0$ (Choice D) D The limit doesn't exist", "hints": ["When dealing with limits that include $\\cos(x)$, it's important to remember that $\\lim_{x\\to\\infty}\\cos(x)$ doesn't exist, as $\\cos(x)$ keeps oscillating between $-1$ and $1$ forever. ${2}$ ${4}$ ${6}$ ${8}$ ${\\llap{-}4}$ ${\\llap{-}6}$ ${\\llap{-}8}$ ${2}$ $y$ $x$ $y=\\cos(x)$ This doesn't necessarily mean that our limit doesn't exist. Think what happens to $\\dfrac{x^2-4}{\\cos(x)}$ as $x$ increases towards positive infinity.", "While $x^2-4$ keeps growing boundlessly, $\\cos(x)$ oscillates from $-1$, to $0$, to $1$, to $0$, to $-1$ again. The result is a graph that goes up and down forever, with vertical asymptotes every now and then. ${5}$ ${10}$ ${15}$ ${\\llap{-}5}$ ${\\llap{-}10}$ ${\\llap{-}15}$ ${50}$ ${100}$ ${150}$ ${\\llap{-}50}$ ${\\llap{-}100}$ ${\\llap{-}150}$ $y$ $x$ $y=\\dfrac{x^2-4}{\\cos(x)}$ This limit doesn't approach any specific value as $x$ increases towards infinity.", "In conclusion, $\\lim_{x\\to\\infty}\\dfrac{x^2-4}{\\cos(x)}$ doesn't exist."]}

File diff suppressed because one or more lines are too long

View File

@ -1,10 +0,0 @@
{
"problem": "For any natural number n, n + 0 = n.",
"level": "SF foundations level 1",
"type": "Algebra",
"solution": [
"Consider some natural number n. The proof will be by induction. Consider the base case n=0. We have 0 + 0 = 0 which holds by the definition of addion.",
"Now the inductive case we want to show n'+1 + 0 = n'+1. Assume it holds for n i.e., n' + 0 = n. So now by the we know the LHS is equal to (n'+0) + 1.",
"Then by the induction hypothesis we know the LHS simplifies to n'+1 completing the proof. Qed."
]
}

View File

@ -1,6 +0,0 @@
{
"problem": "For any natural number n, 0 + n = n.",
"level": "SF foundations level 1",
"type": "Algebra",
"solution": "Consider some natural number n. We now want to show 0 + n = n. Using addition both sides are equal."
}

View File

@ -1,6 +0,0 @@
{
"problem": "For any natural number n, 0 + n = n.",
"level": "SF foundations level 1",
"type": "Algebra",
"solution": "Consider some natural number n. We now want to show 0 + n = n. For that we use the definition of addition to get n = n and conclude both sides are equal (by reflexity)."
}

View File

@ -1,25 +0,0 @@
[
{
"problem": "For any natural number n, 0 + n = n.",
"level": "SF foundations level 1",
"type": "Logical Foundations",
"solution": [
"Consider some natural number n. We want to show 0 + n = n. ",
"By using definition of addition on both sides, LHS and RHS are now equal, done."
]
},
{
"problem": "For any natural number n, n + 0 = n.",
"level": "SF foundations level 1",
"type": "Logical Foundations",
"solution": [
"Consider some natural number n. The proof will be by induction. ",
"The base case n=0, so we have 0 + 0 = 0, which holds by the definition of addion. ",
"Consider the inductive case, so we want to show (k + 1) + 0 = (k + 1) for any k < n assuming the IH holds for such k (IH: k + 0 = k). ",
"By the IH we have (k + 1) + 0 = (k + 1). ",
"By def of addition we have (k + 0) + 1 = (k + 1). ",
"By the induction hypothesis (IH) we have k + 0 = k. ",
"LHS and RHS are equal so proof is complete."
]
}
]

View File

@ -1,368 +0,0 @@
[
{
"nl_problem": "For any natural number n, n + 0 = n.",
"nl_solution": [
"Consider some natural number n. We want to show n + 0 = n. ",
"By using fats of addition on both sides, LHS and RHS are now equal, done."
],
"fl_problem": "theorem n_plus_zero : ∀ n : , n + 0 = n := by",
"fl_solution": [
"-- Prove that n + 0 = n.\n",
"theorem n_plus_zero : ∀ n : , n + 0 = n := by\n",
" -- Consider some n in Nats.\n",
" intro n",
"-- Using facts of addition simplify n + 0 to n.\n",
" rw [Nat.add_zero]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic"
],
"nl_problem_proved_sketch": "For any natural number n, n + 0 = n.",
"nl_solution_proved_sketch": [
"We want to show n + 0 = n. ",
"We have the fact of addition that, n + 0 = n. ",
"Thus, the left-hand side and right-hand side are equal, which completes the proof."
],
"fl_problem_proved_sketch": "theorem n_plus_zero_proved_formal_sketch : ∀ n : , n + 0 = n := by",
"fl_solution_proved_sketch": [
"-- Prove that n + 0 = n via a formal proof sketch",
"theorem n_plus_zero_proved_formal_sketch : ∀ n : , n + 0 = n := by",
" -- We have the fact of addition n + 0 = n, use it to show left and right are equal.",
" have h_nat_add_zero: ∀ n : , n + 0 = n := Nat.add_zero",
" exact h_nat_add_zero"
],
"src_header_fl_solution_proved_sketch": ["import Mathlib.Data.Nat.Basic"],
"nl_problem_proved_sketch_aesop": "For any natural number n, n + 0 = n.",
"nl_solution_proved_sketch_aesop": [
"We want to show n + 0 = n. ",
"We have the fact of addition that, n + 0 = n. ",
"Thus, the left-hand side and right-hand side are equal, which completes the proof."
],
"fl_problem_proved_sketch_aesop": "theorem n_plus_zero_proved_formal_sketch' : ∀ n : , n + 0 = n := by",
"fl_solution_proved_sketch_aesop": [
"-- Prove that n + 0 = n via a formal proof sketch with aesop. ",
"theorem n_plus_zero_proved_formal_sketch' : ∀ n : , n + 0 = n := by",
" -- We have the fact of addition n + 0 = n, use it to show left and right are equal. ",
" have h_nat_add_zero: ∀ n : , n + 0 = n := by aesop",
" exact h_nat_add_zero"
],
"src_header_fl_solution_proved_sketch_aesop": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
]
},
{
"nl_problem": "For any natural number n, 0 + n = n.",
"nl_solution": [
"Consider some natural number n. We want to show 0 + n = n.",
"By using facts of addition and induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem": "theorem zero_plus_n : ∀ n : , 0 + n = n := by",
"fl_solution": [
"-- Prove that 0 + n = n by induction",
"theorem zero_plus_n : ∀ n : , 0 + n = n := by",
"-- Consider some n in Nats.",
"intro n",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: 0 + 0 = 0",
"rw [Nat.add_zero]",
"| succ n ih =>",
"-- Inductive step: assume 0 + n = n, prove 0 + succ n = succ n",
"rw [Nat.add_succ]",
"rw [ih]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic"
],
"nl_problem_proved_sketch": "For any natural number n, 0 + n = n.",
"nl_solution_proved_sketch": [
"We want to show 0 + n = n.",
"By using the fact of addition and performing induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem_proved_sketch": "theorem zero_plus_n_proved_formal_sketch : ∀ n : , 0 + n = n := by",
"fl_solution_proved_sketch": [
"-- Prove that 0 + n = n by induction via a formal proof sketch",
"theorem zero_plus_n_proved_formal_sketch : ∀ n : , 0 + n = n := by",
"-- Consider some n in Nats.",
"intro n",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: 0 + 0 = 0",
"have h_base: 0 + 0 = 0 := by rw [Nat.add_zero]",
"exact h_base",
"| succ n ih =>",
"-- Inductive step: assume 0 + n = n, prove 0 + succ n = succ n",
"have h_inductive: 0 + Nat.succ n = Nat.succ n := by",
"rw [Nat.add_succ]",
"rw [ih]",
"exact h_inductive"
],
"src_header_fl_solution_proved_sketch": [
"import Mathlib.Data.Nat.Basic"
],
"nl_problem_proved_sketch_aesop": "For any natural number n, 0 + n = n.",
"nl_solution_proved_sketch_aesop": [
"We want to show 0 + n = n.",
"By using the fact of addition and performing induction on n, we can prove the statement for both the base case and the inductive step using aesop."
],
"fl_problem_proved_sketch_aesop": "theorem zero_plus_n_proved_formal_sketch' : ∀ n : , 0 + n = n := by",
"fl_solution_proved_sketch_aesop": [
"-- Prove that 0 + n = n by induction via a formal proof sketch with aesop.",
"theorem zero_plus_n_proved_formal_sketch' : ∀ n : , 0 + n = n := by",
"-- Consider some n in Nats.",
"intro n",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: 0 + 0 = 0",
"have h_base: 0 + 0 = 0 := by aesop",
"exact h_base",
"| succ n ih =>",
"-- Inductive step: assume 0 + n = n, prove 0 + succ n = succ n",
"have h_inductive: 0 + Nat.succ n = Nat.succ n := by aesop",
"exact h_inductive"
],
"src_header_fl_solution_proved_sketch_aesop": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
]
},
{
"nl_problem": "For any natural numbers n and m we have commutativity, n + m = m + n.",
"nl_solution": [
"Consider some natural numbers n and m. We want to show n + m = m + n.",
"By using facts of addition and induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem": "theorem add_comm_normal : ∀ n m : , n + m = m + n := by",
"fl_solution": [
"-- Prove that n + m = m + n",
"theorem add_comm_normal : ∀ n m : , n + m = m + n := by",
"-- Consider some n and m in Nats.",
"intros n m",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: When n = 0, we need to show 0 + m = m + 0.",
"-- Using the definition of addition, 0 + m = m and m + 0 = m.",
"rw [Nat.zero_add, Nat.add_zero]",
"| succ n ih =>",
"-- Inductive step: Assume n + m = m + n, we need to show succ n + m = m + succ n.",
"-- We use the fact n + (m + 1) = (n + m) + 1.",
"have plus_n_Sm_normal: ∀ n m : , n + (m + 1) = (n + m) + 1 := by",
" intros n m",
" rw [Nat.add_succ]",
"-- Apply the fact to rewrite succ n + m = (n + m) + 1.",
"rw [Nat.add_succ, Nat.add_zero]",
"rw [← ih]",
"rw [Nat.succ_add]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic"
]
},
{
"nl_problem": "For any natural numbers n and m, n + m = m + n.",
"nl_solution": [
"Consider some natural numbers n and m. We want to show n + m = m + n.",
"By using the fact of addition and performing induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem": "theorem add_comm_proved_formal_sketch : ∀ n m : , n + m = m + n := by",
"fl_solution": [
"-- Prove that n + m = m + n via a formal proof sketch",
"theorem add_comm_proved_formal_sketch : ∀ n m : , n + m = m + n := by",
"-- Consider some n and m in Nats.",
"intros n m",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: When n = 0, we need to show 0 + m = m + 0.",
"-- We have the fact 0 + m = m by the definition of addition.",
"have h_base: 0 + m = m := Nat.zero_add m",
"-- We also have the fact m + 0 = m by the definition of addition.",
"have h_symm: m + 0 = m := Nat.add_zero m",
"-- Combining these, we get 0 + m = m + 0.",
"rw [h_base, h_symm]",
"| succ n ih =>",
"-- Inductive step: Assume n + m = m + n, we need to show succ n + m = m + succ n.",
"-- By the inductive hypothesis, we have n + m = m + n.",
"have h_inductive: n + m = m + n := ih",
"-- proof is:",
"-- We eventually want to flip n + m and simplify to make both sides the same. Thus,",
"-- 1. Note we start with: Nat.succ n + m = m + Nat.succ n, so, pull the succ out from m + Nat.succ n on the right side from the addition using addition facts Nat.add_succ.",
"have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := by rw [Nat.add_succ]",
"-- 2. then to flip m + S n to something like S (n + m) we need to use the IH.",
"have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := by rw [h_inductive]",
"-- 3. Now the n & m are on the correct sides Nat.succ n + m = Nat.succ (n + m), so let's use the def of addition to pull out the succ from the addition on the left using Nat.succ_add.",
"have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := by rw [Nat.succ_add]",
"-- Combining these, we get succ n + m = m + succ n.",
"rw [h_pull_succ_out_from_right, ←h_flip_n_plus_m, h_pull_succ_out_from_left]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic"
]
},
{
"nl_problem": "For any natural numbers n and m, n + m = m + n.",
"nl_solution": [
"Consider some natural numbers n and m. We want to show n + m = m + n.",
"By using the fact of addition and performing induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem": "theorem add_comm_proved_formal_sketch_aesop : ∀ n m : , n + m = m + n := by",
"fl_solution": [
"-- Prove that n + m = m + n via a formal proof sketch with aesop.",
"theorem add_comm_proved_formal_sketch_aesop : ∀ n m : , n + m = m + n := by",
"-- Consider some n and m in Nats.",
"intros n m",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: When n = 0, we need to show 0 + m = m + 0.",
"-- We have the fact 0 + m = m by the definition of addition.",
"have h_base: 0 + m = m := by aesop",
"-- We also have the fact m + 0 = m by the definition of addition.",
"have h_symm: m + 0 = m := by aesop",
"-- Combining these, we get 0 + m = m + 0.",
"rw [h_base, h_symm]",
"| succ n ih =>",
"-- Inductive step: Assume n + m = m + n, we need to show succ n + m = m + succ n.",
"-- By the inductive hypothesis, we have n + m = m + n.",
"have h_inductive: n + m = m + n := by aesop",
"-- proof is:",
"-- We eventually want to flip n + m and simplify to make both sides the same. Thus,",
"-- 1. Note we start with: Nat.succ n + m = m + Nat.succ n, so, pull the succ out from m + Nat.succ n on the right side from the addition using addition facts Nat.add_succ.",
"have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := by aesop",
"-- 2. then to flip m + S n to something like S (n + m) we need to use the IH.",
"have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := by aesop",
"-- 3. Now the n & m are on the correct sides Nat.succ n + m = Nat.succ (n + m), so let's use the def of addition to pull out the succ from the addition on the left using Nat.succ_add.",
"have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := by rw [Nat.succ_add]",
"-- Combining these, we get succ n + m = m + succ n.",
"rw [h_pull_succ_out_from_right, ←h_flip_n_plus_m, h_pull_succ_out_from_left]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
]
},
{
"nl_problem": "Prove that for any natural numbers n, m, and p, n + (m + p) = (n + m) + p.",
"nl_solution": [
"Consider some natural numbers n, m, and p. We want to show n + (m + p) = (n + m) + p.",
"By using facts of addition and induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem": "theorem add_assoc_normal : ∀ n m p : , n + (m + p) = (n + m) + p := by",
"fl_solution": [
"-- Prove that n + (m + p) = (n + m) + p",
"theorem add_assoc_normal : ∀ n m p : , n + (m + p) = (n + m) + p := by",
"-- Consider some n, m, and p in Nats.",
"intros n m p",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: When n = 0, we need to show 0 + (m + p) = (0 + m) + p.",
"-- Using the definition of addition, 0 + (m + p) = m + p and (0 + m) + p = m + p.",
"rw [Nat.zero_add, Nat.zero_add]",
"| succ n ih =>",
"-- Inductive step: Assume n + (m + p) = (n + m) + p, we need to show succ n + (m + p) = (succ n + m) + p.",
"-- proof strategy is, we move succ n out (or in) enough times then use the IH until both sides are the same.",
"-- 1. let's start by pulling out the succ from the left side and have the entire addition inside the succ.",
"rw [Nat.succ_add]",
"-- 2. Now that we have the IH hypothesis appearing inside the left, let's apply it so we have n + (m + p) = (n + m) + p.",
"rw [ih]",
"-- 3. Now that the parentheses (apps of plus) are in the right place for both sides, push the succ on the left twice so both terms are the same.",
"rw [← Nat.succ_add, ← Nat.succ_add]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic"
]
},
{
"nl_problem": "Prove that for any natural numbers n, m, and p, n + (m + p) = (n + m) + p.",
"nl_solution": [
"Consider some natural numbers n, m, and p. We want to show n + (m + p) = (n + m) + p.",
"By using facts of addition and induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem": "theorem add_assoc_proved_formal_sketch : ∀ n m p : , n + (m + p) = (n + m) + p := by",
"fl_solution": [
"-- Prove that n + (m + p) = (n + m) + p",
"theorem add_assoc_proved_formal_sketch : ∀ n m p : , n + (m + p) = (n + m) + p := by",
"-- Consider some n, m, and p in Nats.",
"intros n m p",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: When n = 0, we need to show 0 + (m + p) = (0 + m) + p.",
"-- Using the definition of addition, 0 + (m + p) = m + p and (0 + m) + p = m + p.",
"rw [Nat.zero_add, Nat.zero_add]",
"| succ n ih =>",
"-- Inductive step: Assume n + (m + p) = (n + m) + p, we need to show succ n + (m + p) = (succ n + m) + p.",
"-- proof strategy is, we move succ n out (or in) enough times then use the IH until both sides are the same.",
"-- 1. let's start by pulling out the succ from the left side and have the entire addition inside the succ.",
"have h_pull_add_succ_out_from_left: Nat.succ n + (m + p) = Nat.succ (n + (m + p)) := by rw [Nat.succ_add]",
"-- 2. Now that we have the IH hypothesis appearing inside the left, let's apply it so we have n + (m + p) = (n + m) + p.",
"have h_inside_left_associates: Nat.succ (n + (m + p)) = Nat.succ ((n + m) + p) := by rw [ih]",
"-- 3. Now that the parentheses (apps of plus) are in the right place for both sides, push the succ on the left twice so both terms are the same.",
"have h_push_succ_in_left_twice: Nat.succ ((n + m) + p) = ((Nat.succ n) + m) + p := by rw [← Nat.succ_add, ← Nat.succ_add]",
"-- Combining these, we get succ n + (m + p) = (succ n + m) + p.",
"rw [h_pull_add_succ_out_from_left, h_inside_left_associates, h_push_succ_in_left_twice]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic"
]
},
{
"nl_problem": "Prove that for any natural numbers n, m, and p, n + (m + p) = (n + m) + p.",
"nl_solution": [
"Consider some natural numbers n, m, and p. We want to show n + (m + p) = (n + m) + p.",
"By using facts of addition and induction on n, we can prove the statement for both the base case and the inductive step."
],
"fl_problem": "theorem add_assoc_proved_formal_sketch_aesop : ∀ n m p : , n + (m + p) = (n + m) + p := by",
"fl_solution": [
"-- Prove that n + (m + p) = (n + m) + p via a formal proof sketch with aesop",
"theorem add_assoc_proved_formal_sketch_aesop : ∀ n m p : , n + (m + p) = (n + m) + p := by",
"-- Consider some n, m, and p in Nats.",
"intros n m p",
"-- Perform induction on n.",
"induction n with",
"| zero =>",
"-- Base case: When n = 0, we need to show 0 + (m + p) = (0 + m) + p.",
"-- Using the definition of addition, 0 + (m + p) = m + p and (0 + m) + p = m + p.",
"rw [Nat.zero_add, Nat.zero_add]",
"| succ n ih =>",
"-- Inductive step: Assume n + (m + p) = (n + m) + p, we need to show succ n + (m + p) = (succ n + m) + p.",
"-- proof strategy is, we move succ n out (or in) enough times then use the IH until both sides are the same.",
"-- 1. let's start by pulling out the succ from the left side and have the entire addition inside the succ.",
"have h_pull_add_succ_out_from_left: Nat.succ n + (m + p) = Nat.succ (n + (m + p)) := by rw [Nat.succ_add]",
"-- 2. Now that we have the IH hypothesis appearing inside the left, let's apply it so we have n + (m + p) = (n + m) + p.",
"have h_inside_left_associates: Nat.succ (n + (m + p)) = Nat.succ ((n + m) + p) := by aesop",
"-- 3. Now that the parentheses (apps of plus) are in the right place for both sides, push the succ on the left twice so both terms are the same.",
"have h_push_succ_in_left_twice: Nat.succ ((n + m) + p) = ((Nat.succ n) + m) + p := by rw [← Nat.succ_add, ← Nat.succ_add]",
"-- Combining these, we get succ n + (m + p) = (succ n + m) + p.",
"rw [h_pull_add_succ_out_from_left, h_inside_left_associates, h_push_succ_in_left_twice]"
],
"src_header_fl_solution": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
]
}
]

View File

@ -1,98 +0,0 @@
[
{
"nl_problem": ["Prove that for any natural number n, 0 + n = n."],
"nl_solution": [
"Consider any natural number n. We will prove the statement by induction on n.",
"Base case: When n = 0, we need to show that 0 + 0 = 0. This is true by the definition of addition.",
"Inductive step: Assume that for some natural number n, 0 + n = n. We need to show that 0 + (n + 1) = (n + 1). By the definition of addition and the inductive hypothesis, we have 0 + (n + 1) = (0 + n) + 1 = n + 1. Therefore, the statement holds for n + 1.",
"Thus, by induction, we have proved that for any natural number n, 0 + n = n."
],
"nl_solution_sketch": [
"Consider any natural number n, and do induction on n.",
"Base case: 0 + 0 = 0 by properties of addition.",
"Inductive step we have 0 + n = n. Then 0 + (n + 1) = (0 + n) + 1 = n + 1.",
"Where, 0 + n = n by assumption,qed."
],
"fl_problem": ["theorem zero_plus_n_proved_formal_sketch : ∀ n : , 0 + n = n := "],
"fl_partial_sketch": [
"by\n",
" -- Consider some n in Nats.\n",
" intro n\n",
" -- Perform induction on n.\n",
" induction n with\n",
" | zero =>\n",
" -- Base case: 0 + 0 = 0\n",
" have h_base: 0 + 0 = 0 := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n",
" | succ n ih =>\n",
" -- Inductive step: assume 0 + n = n, prove 0 + succ n = succ n\n",
" have h_inductive: 0 + Nat.succ n = Nat.succ n := <TODO_PROOF_OR_HAMMER>\\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n"
],
"src_header_fl_problem": ["import Mathlib.Data.Nat.Basic"],
"fl_header_sketch": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
],
"path_2_file": "~/gold-ai-olympiad/lean_src_proj/lean_basics/basic_nats_using_mathlib_nats2_simp_no_rw.lean",
"fl_statement_idx": "1"
},
{
"nl_problem": ["Prove that for any natural number n, m, and p, n + (m + p) = (n + m) + p."],
"nl_solution": [
"Consider any natural numbers n, m, and p. We will prove the statement by induction on n.",
"Base case: When n = 0, we need to show that 0 + (m + p) = (0 + m) + p. By the definition of addition, we have 0 + (m + p) = m + p and (0 + m) + p = m + p. Therefore, 0 + (m + p) = (0 + m) + p.",
"Inductive step: Assume that for some natural number n, n + (m + p) = (n + m) + p. We need to show that (n + 1) + (m + p) = ((n + 1) + m) + p.",
"1. First, pull out the successor from the left side to have the entire addition inside the successor: (n + 1) + (m + p) = (n + (m + p)) + 1.",
"2. By the inductive hypothesis, we know that n + (m + p) = (n + m) + p. So we can replace n + (m + p) with (n + m) + p inside the successor: (n + (m + p)) + 1 = ((n + m) + p) + 1.",
"3. Finally, push the successor on the left twice to align both sides: ((n + m) + p) + 1 = (n + 1) + (m + p) = ((n + 1) + m) + p.",
"Thus, by induction, we have proved that for any natural numbers n, m, and p, n + (m + p) = (n + m) + p."
],
"nl_solution_sketch": [
"Consider any natural numbers n, m, and p. We will do induction on n.",
"Base case: 0 + (m + p) = (0 + m) + p by properties of addition.",
"Inductive step, we have n + (m + p) = (n + m) + p. Then (n + 1) + (m + p) = (n + (m + p)) + 1 = ((n + m) + p) + 1.",
"Thus, (n + 1) + (m + p) = ((n + 1) + m) + p, qed."
],
"fl_problem": ["theorem add_assoc_proved_formal_sketch : ∀ n m p : , n + (m + p) = (n + m) + p := "],
"fl_partial_sketch": [
"by\n",
" -- Consider some n, m, and p in Nats.\n",
" intros n m p\n",
" -- Perform induction on n.\n",
" induction n with\n",
" | zero =>\n",
" -- Base case: When n = 0, we need to show 0 + (m + p) = (0 + m) + p.\n",
" -- We have the fact 0 + (m + p) = m + p by the definition of addition.\n",
" have h_base: 0 + (m + p) = m + p := <TODO_PROOF_OR_HAMMER>\n",
" -- We also have the fact (0 + m) + p = m + p by the definition of addition.\n",
" have h_symm: (0 + m) + p = m + p := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n",
" | succ n ih =>\n",
" -- Inductive step: Assume n + (m + p) = (n + m) + p, we need to show succ n + (m + p) = (succ n + m) + p.\n",
" -- By the inductive hypothesis, we have n + (m + p) = (n + m) + p.\n",
" have h_inductive: n + (m + p) = (n + m) + p := <TODO_PROOF_OR_HAMMER>\n",
" -- 1. Let's start by pulling out the succ from left side and have the entire addition inside the succ.\n",
" have h_pull_succ_out_from_left: Nat.succ n + (m + p) = Nat.succ (n + (m + p)) := <TODO_PROOF_OR_HAMMER>\n",
" -- 2. Now that we have the IH hypothesis appearing inside the left, let's apply it so we have n + (m + p) = (n + m) + p.\n",
" have h_inside_left_associates: Nat.succ (n + (m + p)) = Nat.succ ((n + m) + p) := <TODO_PROOF_OR_HAMMER>\n",
" -- 3. Now that the parentheses (apps of plus) are in the right place for both sides, push the succ on the left twice so both terms are the same.\n",
" have h_push_succ_in_left_twice: Nat.succ ((n + m) + p) = ((Nat.succ n) + m) + p := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n"
],
"src_header_fl_problem": ["import Mathlib.Data.Nat.Basic"],
"fl_header_sketch": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
],
"path_2_file": "~/gold-ai-olympiad/lean_src_proj/lean_basics/basic_nats_using_mathlib_nats2_simp_no_rw.lean",
"fl_statement_idx": "4"
}
]

View File

@ -1,117 +0,0 @@
[
{
"nl_problem": ["Prove that for any natural number n, n + 0 = n."],
"nl_solution": [
"Consider any natural number n.",
"Using the properties of addition, we know that adding zero to any number does not change the value of that number.",
"Therefore, we can conclude that n + 0 = n."
],
"nl_solution_sketch": [
"Consider any natural number n.",
"From properties of addition, adding zero does not change its values.",
"Thus, n + 0 = n."
],
"fl_problem": ["theorem n_plus_zero_normal : ∀ n : , n + 0 = n := "],
"fl_partial_sketch": [
"by\n",
" -- We have the fact of addition n + 0 = n, use it to show left and right are equal.\n",
" have h_nat_add_zero: ∀ n : , n + 0 = n := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts with to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n"
],
"src_header_fl_problem": ["import Mathlib.Data.Nat.Basic"],
"fl_header_sketch": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
],
"path_2_file": "~/gold-ai-olympiad/lean_src_proj/lean_basics/basic_nats_using_mathlib_nats2_simp_no_rw.lean",
"fl_statement_idx": "0"
},
{
"nl_problem": ["Prove that for any natural number n, n + (m + 1) = (n + m) + 1."],
"nl_solution": [
"Consider any natural numbers n and m. We want to show that n + (m + 1) = (n + m) + 1.",
"Using the properties of addition, we know that adding 1 to the sum of n and m is the same as first adding m to n and then adding 1.",
"Therefore, we can conclude that n + (m + 1) = (n + m) + 1."
],
"nl_solution_sketch": [
"Consider any natural numbers n and m.",
"From properties of addition, adding 1 to the sum of n and m is the same as first adding m to n and then adding 1.",
"Thus, n + (m + 1) = (n + m) + 1."
],
"fl_problem": ["theorem plus_n_Sm_proved_formal_sketch : ∀ n m : , n + (m + 1) = (n + m) + 1 := "],
"fl_partial_sketch": [
"by\n",
" -- We have the fact of addition n + (m + 1) = (n + m) + 1, use it to show left and right are equal.\n",
" have h_nat_add_succ: ∀ n m : , n + (m + 1) = (n + m) + 1 := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n"
],
"src_header_fl_problem": ["import Mathlib.Data.Nat.Basic"],
"fl_header_sketch": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
],
"path_2_file": "~/gold-ai-olympiad/lean_src_proj/lean_basics/basic_nats_using_mathlib_nats2_simp_no_rw.lean",
"fl_statement_idx": "2"
},
{
"nl_problem": ["Prove that for any natural number n and m, n + m = m + n."],
"nl_solution": [
"Consider any natural numbers n and m. We will prove the statement by induction on n.",
"Base case: When n = 0, we need to show that 0 + m = m + 0. By the definition of addition, we have 0 + m = m and m + 0 = m. Therefore, 0 + m = m + 0.",
"Inductive step: Assume that for some natural number n, n + m = m + n. We need to show that (n + 1) + m = m + (n + 1).",
"1. Start by using the fact that (n + 1) + m = n + (m + 1) and m + (n + 1) = (m + n) + 1.",
"2. By the inductive hypothesis, we have n + m = m + n. So we can replace n + (m + 1) with (m + n) + 1.",
"3. Now, both sides have the same structure, showing that (n + 1) + m = m + (n + 1).",
"Thus, by induction, we have proved that for any natural numbers n and m, n + m = m + n."
],
"nl_solution_sketch": [
"Consider any natural numbers n and m. We will do induction on n.",
"Base case: 0 + m = m + 0 by properties of addition.",
"Inductive step, we have n + m = m + n. Then (n + 1) + m = (n + m) + 1 = (m + n) + 1 = m + (n + 1).",
"Thus, by induction, n + m = m + n, qed."
],
"fl_problem": ["theorem add_comm_proved_formal_sketch : ∀ n m : , n + m = m + n := "],
"fl_partial_sketch": [
"by\n",
" -- Consider some n and m in Nats.\n",
" intros n m\n",
" -- Perform induction on n.\n",
" induction n with\n",
" | zero =>\n",
" -- Base case: When n = 0, we need to show 0 + m = m + 0.\n",
" -- We have the fact 0 + m = m by the definition of addition.\n",
" have h_base: 0 + m = m := <TODO_PROOF_OR_HAMMER>\n",
" -- We also have the fact m + 0 = m by the definition of addition.\n",
" have h_symm: m + 0 = m := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n",
" | succ n ih =>\n",
" -- Inductive step: Assume n + m = m + n, we need to show succ n + m = m + succ n.\n",
" -- By the inductive hypothesis, we have n + m = m + n.\n",
" have h_inductive: n + m = m + n := <TODO_PROOF_OR_HAMMER>\n",
" -- 1. Note we start with: Nat.succ n + m = m + Nat.succ n, so, pull the succ out from m + Nat.succ n on the right side from the addition using addition facts Nat.add_succ.\n",
" have h_pull_succ_out_from_right: m + Nat.succ n = Nat.succ (m + n) := <TODO_PROOF_OR_HAMMER>\n",
" -- 2. then to flip m + S n to something like S (n + m) we need to use the IH.\n",
" have h_flip_n_plus_m: Nat.succ (n + m) = Nat.succ (m + n) := <TODO_PROOF_OR_HAMMER>\n",
" -- 3. Now the n & m are on the correct sides Nat.succ n + m = Nat.succ (n + m), so let's use the def of addition to pull out the succ from the addition on the left using Nat.succ_add.\n",
" have h_pull_succ_out_from_left: Nat.succ n + m = Nat.succ (n + m) := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n"
],
"src_header_fl_problem": ["import Mathlib.Data.Nat.Basic"],
"fl_header_sketch": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
],
"path_2_file": "~/gold-ai-olympiad/lean_src_proj/lean_basics/basic_nats_using_mathlib_nats2_simp_no_rw.lean",
"fl_statement_idx": "3"
}
]

View File

@ -1,106 +0,0 @@
https://www.evernote.com/shard/s410/nl/75276202/2170cbbd-24a1-2d25-da32-bd8f3270d190?title=prompt%20for%20creating%20toy%20example
https://chatgpt.com/c/0ad32608-cbc9-4627-a705-786ed7421826
I want all final responses in this format:
```json
{
"nl_problem": ["Prove that for any natural number n, n + 0 = n."],
"nl_solution": [
"Consider any natural number n.",
"Using the properties of addition, we know that adding zero to any number does not change the value of that number.",
"Therefore, we can conclude that n + 0 = n."
],
"nl_solution_sketch": [
"Consider any natural number n.",
"From properties of addition, adding zero does not change its values.",
"Thus, n + 0 = n."
],
"fl_problem": ["theorem n_plus_zero_normal : ∀ n : , n + 0 = n :="],
"fl_partial_sketch": [
"-- Prove that n + 0 = n via a formal proof sketch with holes to be filled\n",
"theorem n_plus_zero_proved_formal_sketch'' : ∀ n : , n + 0 = n := by\n",
" -- We have the fact of addition n + 0 = n, use it to show left and right are equal.\n",
" have h_nat_add_zero: ∀ n : , n + 0 = n := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts with to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n"
],
"src_header_fl_problem": ["import Mathlib.Data.Nat.Basic"],
"fl_header_sketch": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
],
"path_2_file": "~/gold-ai-olympiad/lean_src_proj/lean_basics/basic_nats_using_mathlib_nats2_simp_no_rw.lean",
"fl_statement_idx": "0"
},
{
"nl_problem": ["Prove that for any natural number n, 0 + n = n."],
"nl_solution": [
"Consider any natural number n. We will prove the statement by induction on n.",
"Base case: When n = 0, we need to show that 0 + 0 = 0. This is true by the definition of addition.",
"Inductive step: Assume that for some natural number n, 0 + n = n. We need to show that 0 + (n + 1) = (n + 1). By the definition of addition and the inductive hypothesis, we have 0 + (n + 1) = (0 + n) + 1 = n + 1. Therefore, the statement holds for n + 1.",
"Thus, by induction, we have proved that for any natural number n, 0 + n = n."
],
"nl_solution_sketch": [
"Consider any natural number n, and do induction on n.",
"Base case: 0 + 0 = 0 by properties of addition.",
"Inductive step we have 0 + n = n. Then 0 + (n + 1) = (0 + n) + 1 = n + 1.",
"Where, 0 + n = n by assumption,qed."
],
"fl_problem": ["theorem zero_plus_n_proved_formal_sketch : ∀ n : , 0 + n = n :="],
"fl_partial_sketch": [
"-- Prove that 0 + n = n by induction via a formal proof sketch with holes to be filled\n",
"theorem zero_plus_n_proved_formal_sketch'' : ∀ n : , 0 + n = n := by\n",
" -- Consider some n in Nats.\n",
" intro n\n",
" -- Perform induction on n.\n",
" induction n with\n",
" | zero =>\n",
" -- Base case: 0 + 0 = 0\n",
" have h_base: 0 + 0 = 0 := <TODO_PROOF_OR_HAMMER>\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n",
" | succ n ih =>\n",
" -- Inductive step: assume 0 + n = n, prove 0 + succ n = succ n\n",
" have h_inductive: 0 + Nat.succ n = Nat.succ n := <TODO_PROOF_OR_HAMMER>\\n",
" -- Combine facts to close goal\n",
" <TODO_PROOF_OR_HAMMER>\n"
],
"src_header_fl_problem": ["import Mathlib.Data.Nat.Basic"],
"fl_header_sketch": [
"import Mathlib.Data.Nat.Basic",
"import Aesop"
]
}
```
I want to translate the following formal proof (solution) in lean 4 to a natural language proof (solution) that a human would write (without lean code in it) and eventually make it into a concise nl_solution_sketch, like the following one:
```human_problem_solution_proof.json
"nl_problem": ["Let \\[f(x) = \\left\\{\n\\begin{array}{cl} ax+3, &\\text{ if }x>2, \\\\\nx-5 &\\text{ if } -2 \\le x \\le 2, \\\\\n2x-b &\\text{ if } x <-2.\n\\end{array}\n\\right.\\]Find $a+b$ if the piecewise function is continuous (which means that its graph can be drawn without lifting your pencil from the paper)."],
"nl_solution_sketch": ["For the piecewise function to be continuous, the cases must \"meet\" at $2$ and $-2$. For example, $ax+3$ and $x-5$ must be equal when $x=2$. This implies $a(2)+3=2-5$, which we solve to get $2a=-6 \\Rightarrow a=-3$. Similarly, $x-5$ and $2x-b$ must be equal when $x=-2$. Substituting, we get $-2-5=2(-2)-b$, which implies $b=3$. So $a+b=-3+3=\\boxed{0}$."]
```
This is my lean 4 fl theorem (fl problem) and fl proof (fl solution):
```
-- Prove that n + (m + p) = (n + m) + p
theorem add_assoc_proved_formal_sketch : ∀ n m p : , n + (m + p) = (n + m) + p := by
-- Consider some n, m, and p in Nats.
intros n m p
-- Perform induction on n.
induction n with
| zero =>
-- Base case: When n = 0, we need to show 0 + (m + p) = (0 + m) + p.
-- Using the definition of addition, 0 + (m + p) = m + p and (0 + m) + p = m + p.
simp [Nat.zero_add, Nat.zero_add]
| succ n ih =>
-- Inductive step: Assume n + (m + p) = (n + m) + p, we need to show succ n + (m + p) = (succ n + m) + p.
-- proof strategy is, we move succ n out (or in) enough times then use the IH until both sides are the same.
-- 1. let's start by pulling out the scc from left side and have the entire addition inside the succ.
have h_pull_add_succ_out_from_left: Nat.succ n + (m + p) = Nat.succ (n + (m + p)) := by simp [Nat.succ_add]
-- 2. Now that we have the IH hypothesis appearing inside the left, let's apply it so we have n + (m + p) = (n + m) + p.
have h_inside_left_associates: Nat.succ (n + (m + p)) = Nat.succ ((n + m) + p) := by simp [ih]
-- 3. Now that the parenthesis (apps of plus) are on the right place for both side, push the succ on the left twice so both terms are the same.
have h_push_succ_in_left_twice: Nat.succ ((n + m) + p) = ((Nat.succ n) + m) + p := by simp [Nat.succ_add, Nat.succ_add]
-- Combining these, we get succ n + (m + p) = (succ n + m) + p.
simp [h_pull_add_succ_out_from_left, h_inside_left_associates, h_push_succ_in_left_twice]
```
use the comments to translate the fl proof (solution) to natural language solution then use that to output a natural language concise sketch. Make the natural language proof (solution) sketch concise with the core elements of the solution proof. Do this by first outputting the natural language solution, distill it into a very concise proof sketch in natural language with only the core components. Output everything in a json code block please:

View File

@ -1,2 +0,0 @@
/build
/lake-packages/*

View File

@ -1,68 +0,0 @@
inductive UnaryNat : Type
| zero : UnaryNat
| succ : UnaryNat → UnaryNat
deriving Repr
#check UnaryNat
#check UnaryNat.zero
#check UnaryNat.succ
#check UnaryNat.succ UnaryNat.zero
-- 0
#eval UnaryNat.zero
-- 1
#eval (UnaryNat.succ UnaryNat.zero)
-- 2
#eval (UnaryNat.succ (UnaryNat.succ UnaryNat.zero))
-- open the namespace for UnaryNat
open UnaryNat
#check zero
def add_left : UnaryNat → UnaryNat → UnaryNat
| zero, n => n
| succ m', n => succ (add_left m' n)
#check add_left zero
#eval add_left zero zero
#eval add_left zero (succ zero)
#eval add_left (succ zero) zero
def add_right (m n : UnaryNat) : UnaryNat :=
match n with
| zero => m
| succ n' => succ (add_right m n')
#eval add_right zero zero
-- todo add_right == add_left
infixl:65 "+" => add_left
#eval zero + zero -- add(0, 0)
-- a + b + c -> add(add(a, b), c) or add(a, add(b, c))
theorem add_zero_is_zero : zero + zero = zero := rfl
-- 0 + n = n
theorem zero_add_n_eq_n : ∀ n : UnaryNat, zero + n = n := by
intro n
rfl
-- simp [add_left]
-- rw [add_left]
-- print the proof term for me please
#print zero_add_n_eq_n
theorem zero_add_n_eq_n' (n : UnaryNat) : zero + n = n := by rfl
#print zero_add_n_eq_n'
-- n + 0 = n
theorem n_add_zero_eq_n : ∀ n : UnaryNat, n + zero = n := by
intro n
induction n with
| zero => apply rfl
-- | succ n' ih => rw [add_left]; rw [ih]
| succ n' ih => rw [add_left, ih]
#print n_add_zero_eq_n
-- comm, assoc, distrib, etc proofs? see software foundations?

View File

@ -1,107 +0,0 @@
---- Example: define unary natural numbers
---- define unary nats
-- define unary natural numbers
inductive UnaryNat : Type
| Zero: UnaryNat
| Succ: UnaryNat -> UnaryNat
-- make unary nats printable
deriving Repr
-- define unary natural numbers
inductive MyNat : Type
| O: MyNat
| S: MyNat -> MyNat
-- make unary nats printable
deriving Repr
----
----
-- bring contents of unary nat into scope
open UnaryNat
-- bring contents of unary nat into scope
open MyNat
----
---- check types and evals
-- check type of unary nat, zero and succ
#check UnaryNat
#check UnaryNat.Zero
#check UnaryNat.Succ
#check UnaryNat.Succ UnaryNat.Zero
#check Succ (Succ Zero)
#eval UnaryNat.Zero
#eval UnaryNat.Succ UnaryNat.Zero
#eval UnaryNat.Succ (UnaryNat.Succ UnaryNat.Zero)
#eval Succ (Succ Zero)
#check O
#eval S (S O)
----
---- define addition for unary natural numbers
-- define addition for unary natural numbers (without explicit names in function declaration)
def add_left : UnaryNat -> UnaryNat -> UnaryNat
| Zero, n => n
| Succ m, n => Succ (add_left m n)
-- define addition for unary natural numbers (with explicit names in function declaration)
def add_left' (m n : UnaryNat) : UnaryNat :=
match m with
| Zero => n
| Succ m' => Succ (add_left' m' n)
-- define addition infix notation
infixl:65 "+l" => add_left'
-- define right addition for unary natural numbers (without explicit names in function declaration)
def add_right : UnaryNat -> UnaryNat -> UnaryNat
| m, Zero => m
| m, Succ n => Succ (add_right m n)
-- define right addition for unary natural numbers (with explicit names in function declaration)
def add_right' (m n : UnaryNat) : UnaryNat :=
match n with
| Zero => m
| Succ n' => Succ (add_right' m n')
-- define right addition infix notation
infixl:65 "+r " => add_right'
---
---- evals for addition
-- eval addition for unary natural numbers left and right
#eval Zero +l Zero
#eval Zero +l (Succ Zero)
#eval (Succ Zero) +l (Succ Zero)
#eval (Succ (Succ Zero)) +r (Succ Zero)
---
---- theorem show non inductive case of addition
-- theorem left addition, 0 + n = n (not inductive proof)
theorem add_left_zero_plus_n_eq_n (n : UnaryNat) : Zero +l n = n := by rfl
-- theorem left addition, 0 + n = n (not inductive proof) with forall statements
theorem add_left_zero_plus_n_eq_n' : Zero +l n = n := by intros; rfl
theorem add_left_zero_plus_n_eq_n'' : Zero +l n = n := by
intros
rfl
-- theorem right addition, n + 0 = n (not inductive proof)
theorem add_right_n_plus_zero_eq_n (n : UnaryNat) : n +r Zero = n := by rfl
-- theorem right addition, n + 0 = n (not inductive proof) with forall statements
theorem add_right_n_plus_zero_eq_n' : n +r Zero = n := by intros; rfl
theorem add_right_n_plus_zero_eq_n'' : n +r Zero = n := by
intros
rfl
----
---- theorem show inductive case of addition
-- theorem left addition, n + 0 = n (inductive proof)
theorem add_left_n_plus_zero_eq_n (n : UnaryNat) : n +l Zero = n := by
induction n with
| Zero => rfl
| Succ n' ih => simp [add_left', ih]
-- theorem left addition, n + 0 = n (inductive proof) with forall and use inductive hypothesis explicitly
theorem add_left_n_plus_zero_eq_n' : ∀ (n : UnaryNat), n +l Zero = n := by
intros n
induction n with
| Zero => rfl
| Succ n' ih => simp [add_left']; assumption

View File

@ -1,25 +0,0 @@
/-
f(x) = m*x + c at x=x' and anything else o.w. (e.g., x)
WTS: lim_{x -> x'} f(x) = m*x' + c
-/
import Mathlib.Data.Real.Basic
-- Define the limit of a function at a point
def limit (f : ) (x' : ) (l : ) : Prop :=
∀ ε : , 0 < ε → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < abs (x - x') ∧ abs (x - x') < δ → abs (f x - l) < ε
-- Define the target function to reason about f(x) = m*x + c at x=x' and anything else o.w. (e.g., x)
noncomputable def lin (m c : ) (x : ) : := m*x + c
noncomputable def f (m c hole_x : ) (x : ) : := if x = hole_x then lin m c x else x
-- Prove the limit of a linear funtion with a hole at the point would be the lin value at the hole i.e., f(x) = m*x + c at x=x' is m*x' + c
theorem limit_of_lin_func_with_hole_eq_lin_func (m c limit_pt_x : ) : limit (f m c hole_x) hole_x (lin m c hole_x) := by
unfold limit
intros ε ε_pos
-- we want 0 < | f(x) - (m*x' + c) | < ε but in format 0 < | x - x' | < δ, so "invert f on both sides and put in δ format"
-- we want 0 < | m*x + c - (m*x' + c) | < ε using def of f not at x'
-- we want 0 < |m| * | x - x' | < ε --> 0 < | x - x' | < ε / |m| so δ = ε / |m|
use ε / abs m
apply And.intro
.

View File

@ -1,8 +0,0 @@
-- Define a singly linked list
inductive Singly_Node (α : Type) : Type
| nil : Singly_Node α
| cons : α → Singly_Node α → Singly_Node α
#check Singly_Node
#check Singly_Node.nil
#check Singly_Node.cons

View File

@ -1,128 +0,0 @@
/-
Task: prove that f(x) = 1/x has a vertical asymptote (unbounded limit) at x = 0 from both sides.
def unbounded_limit (f : ) (c : ) : Prop := ∀ M > 0, ∃ δ > 0, ∀ x, 0 < |x - c| < δ → M < |f x|
theorem one_over_x_has_vertical_asymptote_both_sides : lim_{x -> c} f(x) = +-∞
Proof:
consider any M > 0
now show: ∃ δ > 0, ∀ x, 0 < |x - c| < δ → M < |f x|
So show: ∃ δ > 0, ∀ x, 0 < |x| < δ → M < |1/x|, so in particular don't forget you want -δ < x < δ when guessing δ from goal.
-- guess, delta (s.tif antecedent holds goal holds), so use goal M < |f(x)|, which is M < |1/x| ->
1. M < 1/x (so x > 0 since M>0) -> x < M^⁻¹
2. M < -1/x (so x < 0 since M>0 <-> M <-M) -> M * -x < 1 -> -x < M^⁻¹ -> -M^⁻¹ < -x
1 & 2 means -M^⁻¹ < x < M^⁻¹ <-> |x| < M^⁻¹, so choose δ = M^⁻¹
-- end guess heuristic reasoning
So continue proof by choosing δ = M^⁻¹, and then show that for all x, 0 < |x| < δ -> M < |1/x|
wts: for all x, 0 < |x| < M⁻¹ → M < |1/x|
so consider any x such that 0 < |x| < M⁻¹
I don't really know how to manipulate things freely with abs |.| so I will consider all the cases.
Hypothesis: 0 < |x| < M⁻¹ <-> either 0 < x < M⁻¹ or 0 < -x < M⁻¹ so for both cases we need to show it implies (either because x ∈ cannot satisfy both)
Goal: M < |1/x| <-> M < 1/x for positives or M < -1/x for negatives (either because 1/x ∈ cannot satisfy both)
case 1: I conjecture 0 < x < M⁻¹ -> M < 1/x
1. M < 1/x -> x < 1/M = M^⁻¹ (valid since M > 0, so 1/M > 0, x > 0 so 1/x > 0)
2. 0 < x < M^⁻¹ (as currently required)
case 2: I conjecture 0 < -x < M⁻¹ -> M < -1/x
1. M < -1/x -> M * -x < 1 -> -x < 1/M = M^⁻¹ (valid since M > 0, so 1/M > 0, -x > 0 so -1/x > 0)
2. 0 < -x < M^⁻¹ (as currently required)
Qed.
facts we will need (I think):
identity cancellation (which needs val ≠ 0)
multiply on both sides by some value and inequality doesn't change (or if it does that we show the multiplying val is negative)
simplifying 1 * val = val in either side
perhaps communtativity or/and associativity of multiplication to make sure things cancel simplifying as needed
note: to concluse p q we only need to show p or q, so we can consider them separately (if one holds you can conclude the disjoncution but also if both hold)
-/
import Mathlib.Data.Real.Basic
-- define f(x) = 1/x = x^⁻¹ for x ≠ 0 latter for mathlib notation (less friction during proofs)
noncomputable def f (x : ) : := x⁻¹
#check f
-- note evals don't work so for unit tests we will de a theorem, due to not being "computable"
theorem f_evals_unit_test : f 2 = 1/2 := by simp [f]
#print f_evals_unit_test
-- define unbounded limit (form both sides) as a predicate (proposition)
def unbounded_limit (f : ) (c : ) : Prop := ∀ M : , 0 < M → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < |x - c| ∧ |x - c| < δ → M < |f x|
#check unbounded_limit
#print unbounded_limit
-- note: writing everything in terms of lt since gt is written in terms of lt
-- theorem to prove that f(x) = 1/x has a vertical asymptote (unbounded limit) at x = 0 from both sides
theorem one_over_x_has_vertical_asymptote_both_sides : unbounded_limit f 0 := by
unfold unbounded_limit f
-- consider some M > 0
intro M h_zero_lt_M
-- since goal doesn't have zeros, but we want to use it to match the antecedent, let's simplify the zeros by using the fact x - 0 = 0 at the goal
simp only [sub_zero]
-- guess δ = M^⁻¹ using goal i.e. M < |1/x| so M < 1/x so x < 1/M = M^⁻¹ and -M < -1/x so -x < M^⁻¹ as δ = M^⁻¹ should work
use M⁻¹
-- show 0 < δ = M^⁻¹, first deconstruct the ∧ in the goal
apply And.intro
-- show 0 < M^⁻¹
. exact inv_pos.2 h_zero_lt_M
. --introduce x and hypothesis deconstructed by and
intro x ⟨h_zero_lt_abs_x, h_x_lt_δ⟩
-- unfold abs on hypothesis and goal (since I assume it's harder to manipulate abs |.| function)
#check abs -- abs := mabs (a : α) : α := a ⊔ a⁻¹ == a -> a ⊔ -a
unfold abs at h_x_lt_δ h_zero_lt_abs_x; unfold abs
-- want to show (wts) M < |1/x|, so transform the goal to M < 1/x for x > 0 and M < -1/x for x < 0
-- transform the goal M < x⁻¹ ⊔ -x⁻¹ --> M < x⁻¹ M < -x⁻¹
#check lt_sup_iff -- lt_sup_iff : a < b ⊔ c ↔ a < b a < c
-- simp only [lt_sup_iff] -- also works
apply lt_sup_iff.mpr
-- transform hypothesis 0 < x ⊔ -x --> 0 < x 0 < -x
apply lt_sup_iff.mp at h_zero_lt_abs_x
-- transform hypothesis x ⊔ -x < M⁻¹ --> x < M⁻¹ ∧ -x < M⁻¹
#check sup_lt_iff -- sup_lt_iff : a ⊔ b < c ↔ a < c ∧ b < c
apply sup_lt_iff.mp at h_x_lt_δ
-- to try to close goal M < |1/x|, let's consider both cases by break h_zero_lt_abs_x into both cases 0 < x and 0 < -x and close goals with both cases
#check Or
#check Or.inl
cases h_zero_lt_abs_x with -- TODO: how to name hypothesis with cases in lean4
| inl h_x_pos =>
-- focus on positive target M < x⁻¹ given we are on the x > 0 case x, so also use positive hypothesis x < M⁻¹, simplify any 1 * val = val or val * 1 = val
apply Or.inl
apply And.left at h_x_lt_δ
-- on goal: mul right goal both sides by x (x > 0), then cancel x⁻¹ with mul x (needs x⁻¹ ≠ 0)
have h_x_ne_zero : x ≠ 0 := ne_of_gt h_x_pos
-- mul both sides by M right
#check mul_lt_mul_right -- (a0 : 0 < a) : b * a < c * a ↔ b < c
-- exact (lt_inv h_zero_lt_M h_x_pos).mpr h_x_lt_δ -- also worked!
rw [← mul_lt_mul_right h_x_pos]
nth_rewrite 2 [mul_comm]
#check mul_inv_cancel -- (h : a ≠ 0) : a * a⁻¹ = 1
rw [mul_inv_cancel h_x_ne_zero]
-- move M to the left by mul by M⁻¹ > 0 (needs M⁻¹ ≠ 0 and/or M ≠ 0)
have h_M_inv_lt_zero : 0 < M⁻¹ := inv_pos.2 h_zero_lt_M
rw [← mul_lt_mul_left h_M_inv_lt_zero]
rw [← mul_assoc]
have h_M_ne_zero : M ≠ 0 := ne_of_gt h_zero_lt_M
nth_rewrite 2 [mul_comm]; rewrite [mul_inv_cancel h_M_ne_zero]; simp
assumption
| inr h_x_neg =>
-- focus on negative target M < -x⁻¹ given we are on the x < 0 case x, so also use negative hypothesis -x < M⁻¹, simplify any 1 * val = val or val * 1 = val
apply Or.inr
apply And.right at h_x_lt_δ
-- pass -x⁻¹ to the left and pass M to the right
#check neg_lt -- -a < b ↔ -b < a
-- transform expression -(x⁻¹) to (-x)⁻¹
#check neg_inv -- -a⁻¹ = (-a)⁻¹
rw [neg_inv]
-- multiply both sides by -x (needs -x > 0) left
#check mul_lt_mul_left -- (a0 : 0 < a) : a * b < a * c ↔ b < c
rw [← mul_lt_mul_left h_x_neg]
-- simp only [neg_mul, neg_lt_neg_iff]
have h_neg_x_ne_zero : -x ≠ 0 := ne_of_gt h_x_neg
rw [mul_inv_cancel h_neg_x_ne_zero]
-- move M to the right of the lt by mul right by 0 < M⁻¹ (needs M ≠ 0 for inv cancelation)
have h_M_inv_lt_zero : 0 < M⁻¹ := inv_pos.mpr h_zero_lt_M
rw [← mul_lt_mul_right h_M_inv_lt_zero]
rw [mul_assoc]
have h_M_ne_zero : M ≠ 0 := ne_of_gt h_zero_lt_M
simp only [mul_inv_cancel h_M_ne_zero]
simp
assumption

View File

@ -1,104 +0,0 @@
/-
Task: prove that f(x) = 1/x has a vertical asymptote (unbounded limit) at x = 0 from both sides.
def unbounded_limit (f : ) (c : ) : Prop := ∀ M > 0, ∃ δ > 0, ∀ x, 0 < |x - c| < δ → M < |f x|
theorem one_over_x_has_vertical_asymptote_both_sides : lim_{x -> c} f(x) = +-∞
Proof:
consider any M > 0
now show: ∃ δ > 0, ∀ x, 0 < |x - c| < δ → M < |f x|
So show: ∃ δ > 0, ∀ x, 0 < |x| < δ → M < |1/x|, so in particular don't forget you want -δ < x < δ when guessing δ from goal.
-- guess, delta (s.tif antecedent holds goal holds), so use goal M < |f(x)|, which is M < |1/x| ->
1. M < 1/x (so x > 0 since M>0) -> x < M^⁻¹
2. M < -1/x (so x < 0 since M>0 <-> M <-M) -> M * -x < 1 -> -x < M^⁻¹ -> -M^⁻¹ < -x
1 & 2 means -M^⁻¹ < x < M^⁻¹ <-> |x| < M^⁻¹, so choose δ = M^⁻¹
-- end guess heuristic reasoning
So continue proof by choosing δ = M^⁻¹, and then show that for all x, 0 < |x| < δ -> M < |1/x|
wts: for all x, 0 < |x| < M⁻¹ → M < |1/x|
so consider any x such that 0 < |x| < M⁻¹
I don't really know how to manipulate things freely with abs |.| so I will consider all the cases.
Hypothesis: 0 < |x| < M⁻¹ <-> either 0 < x < M⁻¹ or 0 < -x < M⁻¹ so for both cases we need to show it implies (either because x ∈ cannot satisfy both)
Goal: M < |1/x| <-> M < 1/x for positives or M < -1/x for negatives (either because 1/x ∈ cannot satisfy both)
case 1: I conjecture 0 < x < M⁻¹ -> M < 1/x
1. M < 1/x -> x < 1/M = M^⁻¹ (valid since M > 0, so 1/M > 0, x > 0 so 1/x > 0)
2. 0 < x < M^⁻¹ (as currently required)
case 2: I conjecture 0 < -x < M⁻¹ -> M < -1/x
1. M < -1/x -> M * -x < 1 -> -x < 1/M = M^⁻¹ (valid since M > 0, so 1/M > 0, -x > 0 so -1/x > 0)
2. 0 < -x < M^⁻¹ (as currently required)
Qed.
facts we will need (I think):
identity cancellation (which needs val ≠ 0)
multiply on both sides by some value and inequality doesn't change (or if it does that we show the multiplying val is negative)
simplifying 1 * val = val in either side
perhaps communtativity or/and associativity of multiplication to make sure things cancel simplifying as needed
note: to concluse p q we only need to show p or q, so we can consider them separately (if one holds you can conclude the disjoncution but also if both hold)
-/
import Mathlib.Data.Real.Basic
-- define f(x) = 1/x = x^⁻¹ for x ≠ 0 latter for mathlib notation (less friction during proofs)
noncomputable def f (x : ) : := x⁻¹
#check f
-- note evals don't work so for unit tests we will de a theorem, due to not being "computable"
theorem f_evals_unit_test : f 2 = 1/2 := by simp [f]
#print f_evals_unit_test
-- define unbounded limit (form both sides) as a predicate (proposition)
def unbounded_limit (f : ) (c : ) : Prop := ∀ M : , 0 < M → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < |x - c| ∧ |x - c| < δ → M < |f x|
#check unbounded_limit
#print unbounded_limit
-- note: writing everything in terms of lt since gt is written in terms of lt
-- theorem to prove that f(x) = 1/x has a vertical asymptote (unbounded limit) at x = 0 from both sides
theorem one_over_x_has_vertical_asymptote_both_sides : unbounded_limit f 0 := by
unfold unbounded_limit f
-- consider some M > 0
intro M h_zero_lt_M
-- since goal doesn't have zeros, but we want to use it to match the antecedent, let's simplify the zeros by using the fact x - 0 = 0 at the goal
simp only [sub_zero]
-- guess δ = M^⁻¹ using goal i.e. M < |1/x| so M < 1/x so x < 1/M = M^⁻¹ and -M < -1/x so -x < M^⁻¹ as δ = M^⁻¹ should work
use M⁻¹
-- show 0 < δ = M^⁻¹, first deconstruct the ∧ in the goal
apply And.intro
-- show 0 < M^⁻¹
. exact inv_pos.2 h_zero_lt_M
. --introduce x and hypothesis deconstructed by and
intro x ⟨h_zero_lt_abs_x, h_x_lt_δ⟩
-- unfold abs on hypothesis and goal (since I assume it's harder to manipulate abs |.| function)
#check abs -- abs := mabs (a : α) : α := a ⊔ a⁻¹ == a -> a ⊔ -a
unfold abs at h_x_lt_δ h_zero_lt_abs_x; unfold abs
-- want to show (wts) M < |1/x|, so transform the goal to M < 1/x for x > 0 and M < -1/x for x < 0
-- transform the goal M < x⁻¹ ⊔ -x⁻¹ --> M < x⁻¹ M < -x⁻¹
#check lt_sup_iff -- lt_sup_iff : a < b ⊔ c ↔ a < b a < c
-- simp only [lt_sup_iff] -- also works
apply lt_sup_iff.mpr
-- transform hypothesis 0 < x ⊔ -x --> 0 < x 0 < -x
apply lt_sup_iff.mp at h_zero_lt_abs_x
-- transform hypothesis x ⊔ -x < M⁻¹ --> x < M⁻¹ ∧ -x < M⁻¹
#check sup_lt_iff -- sup_lt_iff : a ⊔ b < c ↔ a < c ∧ b < c
apply sup_lt_iff.mp at h_x_lt_δ
-- to try to close goal M < |1/x|, let's consider both cases by break h_zero_lt_abs_x into both cases 0 < x and 0 < -x and close goals with both cases
#check Or
#check Or.inl
cases h_zero_lt_abs_x with -- TODO: how to name hypothesis with cases in lean4
| inl h_x_pos =>
-- focus on positive target M < x⁻¹ given we are on the x > 0 case x, so also use positive hypothesis x < M⁻¹, simplify any 1 * val = val or val * 1 = val
apply Or.inl
apply And.left at h_x_lt_δ
-- on goal: mul right goal both sides by x (x > 0), then cancel x⁻¹ with mul x (needs x⁻¹ ≠ 0)
-- mul both sides by M right
#check mul_lt_mul_right -- (a0 : 0 < a) : b * a < c * a ↔ b < c
#check lt_inv -- (ha : 0 < a) (hb : 0 < b) : a < b⁻¹ ↔ b < a⁻¹
exact (lt_inv h_zero_lt_M h_x_pos).mpr h_x_lt_δ -- also worked!
| inr h_x_neg =>
-- focus on negative target M < -x⁻¹ given we are on the x < 0 case x, so also use negative hypothesis -x < M⁻¹, simplify any 1 * val = val or val * 1 = val
apply Or.inr
apply And.right at h_x_lt_δ
-- on goal: mul right goal both sides by -x (x < 0), then cancel -x⁻¹ with mul -x (needs -x⁻¹ ≠ 0)
#check lt_inv -- (ha : 0 < a) (hb : 0 < b) : a < b⁻¹ ↔ b < a⁻¹
-- rewrite -x⁻¹ --> -(x⁻¹) so swamp sides of inequality using lt_inv works
rw [neg_inv]
-- have h_neg_x_inv_eq_neg_inv_x : -x⁻¹ = -(x⁻¹) := by simp
exact (lt_inv h_zero_lt_M h_x_neg).mpr h_x_lt_δ -- also worked!

View File

@ -1,135 +0,0 @@
-- /-
-- Task: prove that f(x) = 1/x has a vertical asymptote (unbounded limit) at x = 0 from both sides.
-- def unbounded_limit (f : ) (c : ) : Prop := ∀ M > 0, ∃ δ > 0, ∀ x, 0 < |x - c| < δ → M < |f x|
-- theorem one_over_x_has_vertical_asymptote_both_sides : lim_{x -> c} f(x) = +-∞
-- Proof:
-- consider any M > 0
-- now show: ∃ δ > 0, ∀ x, 0 < |x - c| < δ → M < |f x|
-- So show: ∃ δ > 0, ∀ x, 0 < |x| < δ → M < |1/x|, so in particular don't forget you want -δ < x < δ when guessing δ from goal.
-- -- guess, delta (s.tif antecedent holds goal holds), so use goal M < |f(x)|, which is M < |1/x| ->
-- 1. M < 1/x (so x > 0 since M>0) -> x < M^⁻¹
-- 2. M < -1/x (so x < 0 since M>0 <-> M <-M) -> M * -x < 1 -> -x < M^⁻¹ -> -M^⁻¹ < -x
-- 1 & 2 means -M^⁻¹ < x < M^⁻¹ <-> |x| < M^⁻¹, so choose δ = M^⁻¹
-- -- end guess heuristic reasoning
-- So continue proof by choosing δ = M^⁻¹, and then show that for all x, 0 < |x| < δ -> M < |1/x|
-- wts: for all x, 0 < |x| < M⁻¹ → M < |1/x|
-- so consider any x such that 0 < |x| < M⁻¹
-- I don't really know how to manipulate things freely with abs |.| so I will consider all the cases.
-- Hypothesis: 0 < |x| < M⁻¹ <-> either 0 < x < M⁻¹ or 0 < -x < M⁻¹ so for both cases we need to show it implies (either because x ∈ cannot satisfy both)
-- Goal: M < |1/x| <-> M < 1/x for positives or M < -1/x for negatives (either because 1/x ∈ cannot satisfy both)
-- case 1: I conjecture 0 < x < M⁻¹ -> M < 1/x
-- 1. M < 1/x -> x < 1/M = M^⁻¹ (valid since M > 0, so 1/M > 0, x > 0 so 1/x > 0)
-- 2. 0 < x < M^⁻¹ (as currently required)
-- case 2: I conjecture 0 < -x < M⁻¹ -> M < -1/x
-- 1. M < -1/x -> M * -x < 1 -> -x < 1/M = M^⁻¹ (valid since M > 0, so 1/M > 0, -x > 0 so -1/x > 0)
-- 2. 0 < -x < M^⁻¹ (as currently required)
-- Qed.
-- facts we will need (I think):
-- identity cancellation (which needs val ≠ 0)
-- multiply on both sides by some value and inequality doesn't change (or if it does that we show the multiplying val is negative)
-- simplifying 1 * val = val in either side
-- perhaps communtativity or/and associativity of multiplication to make sure things cancel simplifying as needed
-- note: to concluse p q we only need to show p or q, so we can consider them separately (if one holds you can conclude the disjoncution but also if both hold)
-- -/
-- import Mathlib.Data.Real.Basic
-- -- define f(x) = 1/x = x^⁻¹ for x ≠ 0 latter for mathlib notation (less friction during proofs)
-- noncomputable def f (x : ) : := x⁻¹
-- #check f
-- -- note evals don't work so for unit tests we will de a theorem, due to not being "computable"
-- theorem f_evals_unit_test : f 2 = 1/2 := by simp [f]
-- #print f_evals_unit_test
-- -- define unbounded limit (form both sides) as a predicate (proposition)
-- def unbounded_limit (f : ) (c : ) : Prop := ∀ M : , 0 < M → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < |x - c| ∧ |x - c| < δ → M < |f x|
-- #check unbounded_limit
-- #print unbounded_limit
-- -- note: writing everything in terms of lt since gt is written in terms of lt
-- -- theorem to prove that f(x) = 1/x has a vertical asymptote (unbounded limit) at x = 0 from both sides
-- theorem one_over_x_has_vertical_asymptote_both_sides : unbounded_limit f 0 := by
-- unfold unbounded_limit f
-- -- consider some M > 0
-- intro M h_zero_lt_M
-- -- since goal doesn't have zeros, but we want to use it to match the antecedent, let's simplify the zeros by using the fact x - 0 = 0 at the goal
-- simp only [sub_zero]
-- -- guess δ = M^⁻¹ using goal i.e. M < |1/x| so M < 1/x so x < 1/M = M^⁻¹ and -M < -1/x so -x < M^⁻¹ as δ = M^⁻¹ should work
-- use M⁻¹
-- -- show 0 < δ = M^⁻¹, first deconstruct the ∧ in the goal
-- apply And.intro
-- -- show 0 < M^⁻¹
-- . exact inv_pos.2 h_zero_lt_M
-- . --introduce x and hypothesis deconstructed by and
-- intro x ⟨h_zero_lt_abs_x, h_x_lt_δ⟩
-- -- unfold abs on hypothesis and goal (since I assume it's harder to manipulate abs |.| function)
-- #check abs -- abs := mabs (a : α) : α := a ⊔ a⁻¹ == a -> a ⊔ -a
-- unfold abs at h_x_lt_δ h_zero_lt_abs_x; unfold abs
-- -- want to show (wts) M < |1/x|, so transform the goal to M < 1/x for x > 0 and M < -1/x for x < 0
-- -- transform the goal M < x⁻¹ ⊔ -x⁻¹ --> M < x⁻¹ M < -x⁻¹
-- #check lt_sup_iff -- lt_sup_iff : a < b ⊔ c ↔ a < b a < c
-- -- simp only [lt_sup_iff] -- also works
-- apply lt_sup_iff.mpr
-- -- transform hypothesis 0 < x ⊔ -x --> 0 < x 0 < -x
-- apply lt_sup_iff.mp at h_zero_lt_abs_x
-- -- transform hypothesis x ⊔ -x < M⁻¹ --> x < M⁻¹ ∧ -x < M⁻¹
-- #check sup_lt_iff -- sup_lt_iff : a ⊔ b < c ↔ a < c ∧ b < c
-- apply sup_lt_iff.mp at h_x_lt_δ
-- -- to try to close goal M < |1/x|, let's consider both cases by break h_zero_lt_abs_x into both cases 0 < x and 0 < -x and close goals with both cases
-- #check Or
-- #check Or.inl
-- cases h_zero_lt_abs_x
-- #check h_zero_lt_M
-- #check h✝
-- rename 0 < x h_x_pos
-- -- cases h_zero_lt_abs_x with -- TODO: how to name hypothesis with cases in lean4
-- -- | inl h_x_pos =>
-- -- -- focus on positive target M < x⁻¹ given we are on the x > 0 case x, so also use positive hypothesis x < M⁻¹, simplify any 1 * val = val or val * 1 = val
-- -- apply Or.inl
-- -- apply And.left at h_x_lt_δ
-- -- -- on goal: mul right goal both sides by x (x > 0), then cancel x⁻¹ with mul x (needs x⁻¹ ≠ 0)
-- -- have h_x_ne_zero : x ≠ 0 := ne_of_gt h_x_pos
-- -- -- mul both sides by M right
-- -- #check mul_lt_mul_right -- (a0 : 0 < a) : b * a < c * a ↔ b < c
-- -- -- exact (lt_inv h_zero_lt_M h_x_pos).mpr h_x_lt_δ -- also worked!
-- -- rw [← mul_lt_mul_right h_x_pos]
-- -- nth_rewrite 2 [mul_comm]
-- -- #check mul_inv_cancel -- (h : a ≠ 0) : a * a⁻¹ = 1
-- -- rw [mul_inv_cancel h_x_ne_zero]
-- -- -- move M to the left by mul by M⁻¹ > 0 (needs M⁻¹ ≠ 0 and/or M ≠ 0)
-- -- have h_M_inv_lt_zero : 0 < M⁻¹ := inv_pos.2 h_zero_lt_M
-- -- rw [← mul_lt_mul_left h_M_inv_lt_zero]
-- -- rw [← mul_assoc]
-- -- have h_M_ne_zero : M ≠ 0 := ne_of_gt h_zero_lt_M
-- -- nth_rewrite 2 [mul_comm]; rewrite [mul_inv_cancel h_M_ne_zero]; simp
-- -- assumption
-- -- | inr h_x_neg =>
-- -- -- focus on negative target M < -x⁻¹ given we are on the x < 0 case x, so also use negative hypothesis -x < M⁻¹, simplify any 1 * val = val or val * 1 = val
-- -- apply Or.inr
-- -- apply And.right at h_x_lt_δ
-- -- -- pass -x⁻¹ to the left and pass M to the right
-- -- #check neg_lt -- -a < b ↔ -b < a
-- -- -- transform expression -(x⁻¹) to (-x)⁻¹
-- -- #check neg_inv -- -a⁻¹ = (-a)⁻¹
-- -- rw [neg_inv]
-- -- -- multiply both sides by -x (needs -x > 0) left
-- -- #check mul_lt_mul_left -- (a0 : 0 < a) : a * b < a * c ↔ b < c
-- -- rw [← mul_lt_mul_left h_x_neg]
-- -- -- simp only [neg_mul, neg_lt_neg_iff]
-- -- have h_neg_x_ne_zero : -x ≠ 0 := ne_of_gt h_x_neg
-- -- rw [mul_inv_cancel h_neg_x_ne_zero]
-- -- -- move M to the right of the lt by mul right by 0 < M⁻¹ (needs M ≠ 0 for inv cancelation)
-- -- have h_M_inv_lt_zero : 0 < M⁻¹ := inv_pos.mpr h_zero_lt_M
-- -- rw [← mul_lt_mul_right h_M_inv_lt_zero]
-- -- rw [mul_assoc]
-- -- have h_M_ne_zero : M ≠ 0 := ne_of_gt h_zero_lt_M
-- -- simp only [mul_inv_cancel h_M_ne_zero]
-- -- simp
-- -- assumption

View File

@ -1,6 +0,0 @@
{
"problem": "How many vertical asymptotes does 1/x have around 0 from the right?",
"level": "Level 2",
"type": "Algebra",
"solution": "$1/x$ goes to positive infinity from the right as $x$ goes to zero and nowhere else, so it has $\\boxed{1}$ veritcal asymptote."
}

View File

@ -1,6 +0,0 @@
{
"problem": "Show that 1/x has an unbounded limit from the right as x approaches zero?",
"level": "Level 2",
"type": "Algebra",
"solution": "..."
}

View File

@ -1,96 +0,0 @@
/-
theorem: lim_{x -> c+} f(x) = +infinity
x + infinit = +infinity
lim_{x -> c} f(x) = L
∀ ε > 0, ∃ δ > 0, 0 < |x - c| < δ → 0 < |f(x) - L| < ε
L = + infinity
consider some ε > 0
0 < |f(x) - L| < ε
0 < |f(x) - +infinity| < ε
--> this formalization doens't seem promising
theorem limit_of_reciprocal_of_x_is_unbounded: lim_{x -> 0+} 1/x = +infinity
∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
-- unboudned limit := "for any M, there exists a sufficiently close x s.t. f(x) is strictly greater than M"
∀ M: , 0 < M, ∃ δ : , 0 < δ, ∀ x : , 0 < x - c < δ → M < f(x)
proof:
consider some M > 0 (intro M)
-- choose delta, M < f(x) --> M < 1/x --> 1/M > x --> x < M⁻¹
δ = M⁻¹
. show 0 < δ
fact M > 0 --> M⁻¹ > 0 (by lemma in lean, division by positive number)
0 < x - c -> rewrite
-> 0 < x
x - c < δ -> rewrite
-> x < M⁻¹
(WTS: M < x⁻¹)
x < M⁻¹
-- multiply both sides by x⁻¹ if x⁻¹ > 0 (lemma, have stmt)
-> 0 < x --> x⁻¹ > 0
x⁻¹ * x < M^⁻¹ * x⁻¹
by identity x⁻¹ * x = 1 of fields (lemma in lean or automation)
1 < M⁻¹ * x⁻¹
-- multiply both sides by M if M > 0
1 < M⁻¹ * x⁻¹
M * 1 < M * M⁻¹ * x⁻¹
-- identity
M < x⁻¹
Qed
-/
-- import real numbers form mathlib
import Mathlib.Data.Real.Basic
noncomputable def f (x : ) : := x⁻¹
#print f
#check f
#check f 1
-- #eval f 1
-- theorem any_R : -> R := λ x : , x -- TODO
theorem unit_test_f_1 : f 1 = 1 := by simp [f]
theorem unit_test_f_2 : f 2 = 1/2 := by simp [f]
noncomputable def f' (x : ) : := 1/x
theorem units_f_eq_f' : ∀ x : , f x = f' x := by simp [f, f']
#print units_f_eq_f'
-- lim_{x -> c+} f(x) = +infinity := ∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
def unbounded_limit (f : -> ) (c : ) : Prop :=
∀ M : , 0 < M → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < x - c ∧ x - c < δ → M < f x
-- show 1/x is unbounded as x -> 0 (or 1/x has a veritcal asymptote at x = 0)
theorem limit_of_reciprocal_of_x_is_unbounded: unbounded_limit f 0 := by
unfold unbounded_limit f
-- choose M : and is M > 0
intro M h_M_pos
-- choose delta = M⁻¹ by a tactic
use M⁻¹
-- deconstruct the constructor Left ∧ Right = And(Left, Right) to Left, Right using a tactic
apply And.intro
. exact (by simp [h_M_pos]) -- TODO try to find the lemma in mathlib to prove this
. intro x ⟨h_x_pos, h_x_lt_M⟩
-- rewrite x - 0 to x using a tactic for sub
rw [sub_zero] at h_x_pos h_x_lt_M
-- multiply both sides by x we know 0 < x so it should work, using a tactic rewrite
-- mul_lt_mul_left: (a0 : 0 < a) : a * b < a * c ← b < c
rw [← mul_lt_mul_left h_x_pos]
-- rewrite x * x⁻¹ = 1
-- mul_inv_cancel: a ≠ 0 → a * a⁻¹ = 1
have h_x_neq_zero: x ≠ 0 := by exact ne_of_gt h_x_pos
rw [mul_inv_cancel h_x_neq_zero]
have h_M_inv_pos: 0 < M⁻¹ := by simp [h_M_pos]
-- multiply both sides by M⁻¹ on the right
rw [← mul_lt_mul_right h_M_inv_pos]
-- rewrite 1 * M = M
rw [one_mul]
-- rewrite M * M⁻¹ = 1
-- mul_inv_cancel: a ≠ 0 → a * a⁻¹ = 1
have h_M_neq_zero: M ≠ 0 := by exact ne_of_gt h_M_pos
-- have h_M_inv: M * M⁻¹ = 1 := by rw [mul_inv_cancel h_M_neq_zero]
rw [mul_inv_cancel_right₀ h_M_neq_zero x]
assumption

View File

@ -1,39 +0,0 @@
/-
-/
import Mathlib.Data.Real.Basic
-- define 1/x (reciprical) for reals
noncomputable def f (x : ): := x⁻¹
#check f
-- unit test that f 1 = 1, f 2 = 1/2
theorem test_f1 : f 1 = 1 := by simp[f]
theorem test_f2 : f 2 = 2⁻¹ := by simp[f]
#print test_f1
#print test_f2
-- set_option pp.notation false
-- The limit of f x as x approaches c+ from the right is +infinity i.e., limit is unbounded from the right
-- i.e., lim_{x -> c+} f(x) = +infinity
def has_unbounded_limit_right (f: -> ) (c : ) : Prop :=
∀ M : , 0 < M → ∃ δ, 0 < δ ∧ ∀ x : , 0 < x - c ∧ x - c < δ → M < f x
#print has_unbounded_limit_right
theorem reciprocal_has_unbounded_limit_right : has_unbounded_limit_right f 0 := by
unfold has_unbounded_limit_right
intro M h_0_lt_M
-- select delta that works since func is 1/x then anything less than 1/M will make f x be greater than M (so it should work)
use M⁻¹
-- TODO split (what did scott want with this, read)
constructor
. rwa [inv_pos]
. -- consider any x with 0 < x - 0 < M⁻¹ but introduce both hypothesis 0 < x - 0 and x - 0 < M⁻¹
intro x ⟨h_x_pos, h_x_lt_δ⟩
-- rintro x ⟨h_x_pos, h_x_lt_δ⟩ -- TODO tomorrow, why did scott do this?
-- rewrite both hypothesis using fact m - 0 = m
rw [sub_zero] at h_x_pos h_x_lt_δ
unfold f
-- multiply both sides of h_x_lt_δ by x⁻¹ on the left using mul_lt_mul_right
rwa [propext (lt_inv h_0_lt_M h_x_pos)]
-- state p f = 1 todo: https://proofassistants.stackexchange.com/questions/3800/given-some-proposition-in-lean-4-how-do-we-state-a-theorem-saying-that-we-want

View File

@ -1,99 +0,0 @@
/-
theorem: lim_{x -> c+} f(x) = +infinity
x + infinit = +infinity
lim_{x -> c} f(x) = L
∀ ε > 0, ∃ δ > 0, 0 < |x - c| < δ → 0 < |f(x) - L| < ε
L = + infinity
consider some ε > 0
0 < |f(x) - L| < ε
0 < |f(x) - +infinity| < ε
--> this formalization doens't seem promising
theorem limit_of_reciprocal_of_x_is_unbounded: lim_{x -> 0+} 1/x = +infinity
∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
-- unboudned limit := "for any M, there exists a sufficiently close x s.t. f(x) is strictly greater than M"
∀ M: , 0 < M, ∃ δ : , 0 < δ, ∀ x : , 0 < x - c < δ → M < f(x)
proof:
consider some M > 0 (intro M)
-- choose delta, M < f(x) --> M < 1/x --> 1/M > x --> x < M⁻¹
δ = M⁻¹
. show 0 < δ
fact M > 0 --> M⁻¹ > 0 (by lemma in lean, division by positive number)
0 < x - c -> rewrite
-> 0 < x
x - c < δ -> rewrite
-> x < M⁻¹
(WTS: M < x⁻¹)
x < M⁻¹
-- multiply both sides by x⁻¹ if x⁻¹ > 0 (lemma, have stmt)
-> 0 < x --> x⁻¹ > 0
x⁻¹ * x < M^⁻¹ * x⁻¹
by identity x⁻¹ * x = 1 of fields (lemma in lean or automation)
1 < M⁻¹ * x⁻¹
-- multiply both sides by M if M > 0
1 < M⁻¹ * x⁻¹
M * 1 < M * M⁻¹ * x⁻¹
-- identity
M < x⁻¹
Qed
-/
-- import real numbers form mathlib
import Mathlib.Data.Real.Basic
noncomputable def f (x : ) : := x⁻¹
#print f
#check f
#check f 1
-- #eval f 1
-- theorem any_R : -> R := λ x : , x -- TODO
theorem unit_test_f_1 : f 1 = 1 := by simp [f]
theorem unit_test_f_2 : f 2 = 1/2 := by simp [f]
noncomputable def f' (x : ) : := 1/x
theorem units_f_eq_f' : ∀ x : , f x = f' x := by simp [f, f']
#print units_f_eq_f'
-- lim_{x -> c+} f(x) = +infinity := ∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
def unbounded_limit (f : -> ) (c : ) : Prop :=
∀ M : , 0 < M → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < x - c ∧ x - c < δ → M < f x
-- show 1/x is unbounded as x -> 0 (or 1/x has a veritcal asymptote at x = 0)
theorem limit_of_reciprocal_of_x_is_unbounded: unbounded_limit f 0 := by
unfold unbounded_limit f
-- choose M : and is M > 0
intro M h_M_pos
-- choose delta = M⁻¹ by a tactic
use M⁻¹
-- deconstruct the constructor Left ∧ Right = And(Left, Right) to Left, Right using a tactic
apply And.intro
. exact (by simp [h_M_pos]) -- TODO try to find the lemma in mathlib to prove this
. intro x ⟨h_x_pos, h_x_lt_M⟩
-- rewrite x - 0 to x using a tactic for sub
rw [sub_zero] at h_x_pos h_x_lt_M
-- using rewrite do M < x⁻¹ → M * x < x⁻¹ * x by mulitpling both sides by x on the right
-- #print mul_lt_mul_right -- (a0 : 0 < a) : b * a < c * a ↔ b < c
rw [← mul_lt_mul_right h_x_pos]
-- using rewrite let's cancel the x's i.e. x * x⁻¹ = 1 or use the multiplicatitve identity lemma
-- apply commutativity of multiplication to the end part of the equation, to goal part 2
nth_rewrite 2 [mul_comm]
-- (h : a ≠ 0) : a * a⁻¹ = 1 let define a lemma for x ≠ 0
have h_x_neq_zero: x ≠ 0 := ne_of_gt h_x_pos
rw [mul_inv_cancel h_x_neq_zero]
-- let's (left) multiply both sides by M⁻¹ then cancel the M's then simplify M⁻¹*1 = M⁻¹ the close proof
have h_M_inv_pos: 0 < M⁻¹ := by simp [h_M_pos]
rw [← mul_lt_mul_left h_M_inv_pos]
rw [mul_one]
-- rewrite M⁻¹ * M * x = M * M⁻¹ * x via associativity of multiplication
-- (a b c : G) : a * b * c = a * (b * c)
rw [← mul_assoc]
-- cancel the M's then simplify M⁻¹*1 = M⁻¹ the close proof
have h_M_neq_zero: M ≠ 0 := ne_of_gt h_M_pos
-- mul_inv_cancel : (h : a ≠ 0) : a * a⁻¹ = 1
simp [h_M_neq_zero]
assumption

View File

@ -1,103 +0,0 @@
/-
theorem: lim_{x -> c+} f(x) = +infinity
x + infinit = +infinity
lim_{x -> c} f(x) = L
∀ ε > 0, ∃ δ > 0, 0 < |x - c| < δ → 0 < |f(x) - L| < ε
L = + infinity
consider some ε > 0
0 < |f(x) - L| < ε
0 < |f(x) - +infinity| < ε
--> this formalization doens't seem promising
theorem limit_of_reciprocal_of_x_is_unbounded: lim_{x -> 0+} 1/x = +infinity
∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
-- unboudned limit := "for any M, there exists a sufficiently close x s.t. f(x) is strictly greater than M"
∀ M: , 0 < M, ∃ δ : , 0 < δ, ∀ x : , 0 < x - c < δ → M < f(x)
proof:
consider some M > 0 (intro M)
-- choose delta, M < f(x) --> M < 1/x --> 1/M > x --> x < M⁻¹
δ = M⁻¹
. show 0 < δ
fact M > 0 --> M⁻¹ > 0 (by lemma in lean, division by positive number)
0 < x - c -> rewrite
-> 0 < x
x - c < δ -> rewrite
-> x < M⁻¹
(WTS: M < x⁻¹)
x < M⁻¹
-- multiply both sides by x⁻¹ if x⁻¹ > 0 (lemma, have stmt)
-> 0 < x --> x⁻¹ > 0
x⁻¹ * x < M^⁻¹ * x⁻¹
by identity x⁻¹ * x = 1 of fields (lemma in lean or automation)
1 < M⁻¹ * x⁻¹
-- multiply both sides by M if M > 0
1 < M⁻¹ * x⁻¹
M * 1 < M * M⁻¹ * x⁻¹
-- identity
M < x⁻¹
Qed
-/
-- import real numbers form mathlib
import Mathlib.Data.Real.Basic
noncomputable def f (x : ) : := x⁻¹
#print f
#check f
#check f 1
-- #eval f 1
-- theorem any_R : -> R := λ x : , x -- TODO
theorem unit_test_f_1 : f 1 = 1 := by simp [f]
theorem unit_test_f_2 : f 2 = 1/2 := by simp [f]
noncomputable def f' (x : ) : := 1/x
theorem units_f_eq_f' : ∀ x : , f x = f' x := by simp [f, f']
#print units_f_eq_f'
-- lim_{x -> c+} f(x) = +infinity := ∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
def unbounded_limit (f : -> ) (c : ) : Prop :=
∀ M : , 0 < M → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < x - c ∧ x - c < δ → M < f x
-- show 1/x is unbounded as x -> 0 (or 1/x has a veritcal asymptote at x = 0)
theorem limit_of_reciprocal_of_x_is_unbounded: unbounded_limit f 0 := by
unfold unbounded_limit f
-- choose M : and is M > 0
intro M h_M_pos
-- choose delta = M⁻¹ by a tactic
use M⁻¹
-- deconstruct the constructor Left ∧ Right = And(Left, Right) to Left, Right using a tactic
apply And.intro
. exact (by simp [h_M_pos]) -- TODO try to find the lemma in mathlib to prove this
. intro x ⟨h_x_pos, h_x_lt_M⟩
-- rewrite x - 0 to x using a tactic for sub
rw [sub_zero] at h_x_pos h_x_lt_M
-- using rewrite do M < x⁻¹ → M * x < x⁻¹ * x by mulitpling both sides by x on the right
-- #print mul_lt_mul_right -- (a0 : 0 < a) : b * a < c * a ↔ b < c
rw [← mul_lt_mul_right h_x_pos]
-- using rewrite let's cancel the x's i.e. x * x⁻¹ = 1 or use the multiplicatitve identity lemma
-- apply commutativity of multiplication to the end part of the equation, to goal part 2
nth_rewrite 2 [mul_comm]
-- (h : a ≠ 0) : a * a⁻¹ = 1 let define a lemma for x ≠ 0
have h_x_neq_zero: x ≠ 0 := ne_of_gt h_x_pos
rw [mul_inv_cancel h_x_neq_zero]
-- let's (left) multiply both sides by M⁻¹ then cancel the M's then simplify M⁻¹*1 = M⁻¹ the close proof
have h_M_inv_pos: 0 < M⁻¹ := by simp [h_M_pos]
rw [← mul_lt_mul_left h_M_inv_pos]
rw [mul_one]
-- rewrite M⁻¹ * M * x = M * M⁻¹ * x via associativity of multiplication
-- (a b c : G) : a * b * c = a * (b * c)
rw [← mul_assoc]
-- cancel the M's then simplify M⁻¹*1 = M⁻¹ the close proof
have h_M_neq_zero: M ≠ 0 := ne_of_gt h_M_pos
-- mul_inv_cancel : (h : a ≠ 0) : a * a⁻¹ = 1
nth_rewrite 2 [mul_comm]
-- -- use mul identity (h : a ≠ 0) : a * a⁻¹ = 1 to cancel the M's
rw [mul_inv_cancel h_M_neq_zero]
rw [mul_comm]
rw [mul_one]
assumption

View File

@ -1,101 +0,0 @@
/-
theorem: lim_{x -> c+} f(x) = +infinity
x + infinit = +infinity
lim_{x -> c} f(x) = L
∀ ε > 0, ∃ δ > 0, 0 < |x - c| < δ → 0 < |f(x) - L| < ε
L = + infinity
consider some ε > 0
0 < |f(x) - L| < ε
0 < |f(x) - +infinity| < ε
--> this formalization doens't seem promising
theorem limit_of_reciprocal_of_x_is_unbounded: lim_{x -> 0+} 1/x = +infinity
∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
-- unboudned limit := "for any M, there exists a sufficiently close x s.t. f(x) is strictly greater than M"
∀ M: , 0 < M, ∃ δ : , 0 < δ, ∀ x : , 0 < x - c < δ → M < f(x)
proof:
consider some M > 0 (intro M)
-- choose delta, M < f(x) --> M < 1/x --> 1/M > x --> x < M⁻¹
δ = M⁻¹
. show 0 < δ
fact M > 0 --> M⁻¹ > 0 (by lemma in lean, division by positive number)
0 < x - c -> rewrite
-> 0 < x
x - c < δ -> rewrite
-> x < M⁻¹
(WTS: M < x⁻¹)
x < M⁻¹
-- multiply both sides by x⁻¹ if x⁻¹ > 0 (lemma, have stmt)
-> 0 < x --> x⁻¹ > 0
x⁻¹ * x < M^⁻¹ * x⁻¹
by identity x⁻¹ * x = 1 of fields (lemma in lean or automation)
1 < M⁻¹ * x⁻¹
-- multiply both sides by M if M > 0
1 < M⁻¹ * x⁻¹
M * 1 < M * M⁻¹ * x⁻¹
-- identity
M < x⁻¹
Qed
-/
-- import real numbers form mathlib
import Mathlib.Data.Real.Basic
noncomputable def f (x : ) : := x⁻¹
#print f
#check f
#check f 1
-- #eval f 1
-- theorem any_R : -> R := λ x : , x -- TODO
theorem unit_test_f_1 : f 1 = 1 := by simp [f]
theorem unit_test_f_2 : f 2 = 1/2 := by simp [f]
noncomputable def f' (x : ) : := 1/x
theorem units_f_eq_f' : ∀ x : , f x = f' x := by simp [f, f']
#print units_f_eq_f'
-- lim_{x -> c+} f(x) = +infinity := ∀ M > 0, ∃ δ > 0, ∀ x : , 0 < x - c < δ → f(x) > M
def unbounded_limit (f : -> ) (c : ) : Prop :=
∀ M : , 0 < M → ∃ δ : , 0 < δ ∧ ∀ x : , 0 < x - c ∧ x - c < δ → M < f x
-- show 1/x is unbounded as x -> 0 (or 1/x has a veritcal asymptote at x = 0)
theorem limit_of_reciprocal_of_x_is_unbounded: unbounded_limit f 0 := by
unfold unbounded_limit f
-- choose M : and is M > 0
intro M h_M_pos
-- choose delta = M⁻¹ by a tactic
use M⁻¹
-- deconstruct the constructor Left ∧ Right = And(Left, Right) to Left, Right using a tactic
apply And.intro
. exact (by simp [h_M_pos]) -- TODO try to find the lemma in mathlib to prove this
. intro x ⟨h_x_pos, h_x_lt_M⟩
-- rewrite x - 0 to x using a tactic for sub
rw [sub_zero] at h_x_pos h_x_lt_M
-- using rewrite do M < x⁻¹ → M * x < x⁻¹ * x by mulitpling both sides by x on the right
-- #print mul_lt_mul_right -- (a0 : 0 < a) : b * a < c * a ↔ b < c
rw [←mul_lt_mul_right h_M_pos] at h_x_lt_M
-- #print mul_inv_cancel
-- mul_inv_cancel: a ≠ 0 → a * a⁻¹ = 1
nth_rewrite 2 [mul_comm] at h_x_lt_M
have h_M_neq_zero : M ≠ 0 := ne_of_gt h_M_pos
rw [mul_inv_cancel h_M_neq_zero] at h_x_lt_M
-- multiply both sides by x⁻¹ on the left
have h_x_inv_pos : 0 < x⁻¹ := inv_pos.mpr h_x_pos
rw [← mul_lt_mul_left h_x_inv_pos] at h_x_lt_M
-- apply associativity of mul
rw [← mul_assoc] at h_x_lt_M
-- mul_inv_cancel: a ≠ 0 → a * a⁻¹ = 1
nth_rewrite 2 [mul_comm] at h_x_lt_M
-- cancel the x * x⁻¹ to 1
have h_x_neq_zero : x ≠ 0 := ne_of_gt h_x_pos
rw [mul_inv_cancel h_x_neq_zero] at h_x_lt_M
-- apply 1 * M = M
rw [one_mul] at h_x_lt_M
rw [mul_comm] at h_x_lt_M
rw [one_mul] at h_x_lt_M
assumption

View File

@ -1 +0,0 @@
def hello := "world"

View File

@ -1,34 +0,0 @@
-- {
-- "problem": "Let $t(x) = \\sqrt{3x+1}$ and $f(x)=5-t(x)$. What is $t(f(5))$?",
-- "level": "Level 4",
-- "type": "Algebra",
-- "solution": "We first evaluate $f(5) = 5 -t(5) = 5-\\sqrt{5\\cdot3+1}=1$. Thus $t(f(5))=t(1)=\\sqrt{3\\cdot1 + 1}=\\boxed{2}$."
-- }
import Mathlib.Data.Real.Basic
import Mathlib.Data.Real.Sqrt
import Mathlib.Algebra.GroupPower.Order
noncomputable def t (x : ) : := Real.sqrt (3 * x + 1)
noncomputable def f (x : ) : := 5 - t x
theorem solve_t_at_5: t 5 = 4 := by
have h0 : Real.sqrt 4 ^ 2 = 4 := Real.sq_sqrt (Nat.ofNat_nonneg _)
have h1 : 3 * 5 + 1 = 4^2 := by rfl
have h2 : Real.sqrt (3 * 5 + 1) = Real.sqrt 4^2:= by sorry
unfold t
rw[h2, h0]
theorem solve_f_at_5: f 5 = 1 := by
unfold f
have h: t 5 = 4 := by apply solve_t_at_5
rw[h]
ring
theorem solve_t_f_at_5: t (f 5) = 2 := by
unfold t
have h0: f 5 = 1 := by apply solve_f_at_5
have h1: 3 * 1 + 1 = 2^2 := by rfl
have h2: Real.sqrt (3 * 1 + 1) = Real.sqrt 2^2 := by sorry
have h3: Real.sqrt 2^2 = 2 := Real.sq_sqrt (Nat.ofNat_nonneg _)
rw[h0, h2, h3]

View File

@ -1,26 +0,0 @@
-- {
-- "problem": "The perimeter of a rectangle is 24 inches. What is the number of square inches in the maximum possible area for this rectangle?",
-- "level": "Level 3",
-- "type": "Algebra",
-- "solution": "Let one pair of parallel sides have length $x$ and the other pair of parallel sides have length $12-x$. This means that the perimeter of the rectangle is $x+x+12-x+12-x=24$ as the problem states. The area of this rectangle is $12x-x^2$. Completing the square results in $-(x-6)^2+36\\le 36$ since $(x-6)^2\\ge 0$, so the maximum area of $\\boxed{36}$ is obtained when the rectangle is a square of side length 6 inches."
-- }
-- Note: translating this to 2x + 2y = 24, what is xy?
import Mathlib.Data.Real.Basic
import Mathlib.Algebra.Group.Defs
import Mathlib.Algebra.Ring.Defs
import Mathlib.Tactic.Linarith.Frontend
def valid_perimeter (x y : ) : Prop :=
2 * x + 2 * y = 24
def area (x y : ) := x * y
theorem rewrite_y_as_x: valid_perimeter x y → y = 12 - x := by
unfold valid_perimeter
intro p
have h0 : 24 = 2 * 12 := by rfl
have h1 : 2 * x + 2 * y = 2 * (x + y) := by ring
have h2 : 2 * (x + y) = 2 * 12 → x + y = 12 := by sorry
have h3 : x + y = 12 → y = 12 - x := by sorry
rw[h0, h1, h2] at p

View File

@ -1,19 +0,0 @@
-- {
-- "problem": "If $\\sqrt{2\\sqrt{t-2}} = \\sqrt[4]{7 - t}$, then find $t$.",
-- "level": "Level 4",
-- "type": "Algebra",
-- "solution": "We raise both sides to the fourth power, which is equivalent to squaring twice, in order to get rid of the radicals. The left-hand side becomes $$\\left(\\sqrt{2\\sqrt{t-2}}\\right)^4 = \\left(2\\sqrt{t-2}\\right)^2 = 4 \\cdot (t-2) = 4t-8.$$The right-hand side becomes $\\left(\\sqrt[4]{7-t}\\right)^4 = 7-t$. Setting them equal, $$4t-8 = 7-t \\quad\\Longrightarrow\\quad 5t = 15,$$and $t = \\boxed{3}$. Checking, we find that this value does indeed satisfy the original equation."
-- }
import Mathlib.Data.Real.Basic
import Mathlib.Data.Nat.Pow
noncomputable def a (t : ) : := (2 * (t - 2) ^ (1 / 2)) ^ (1/2)
noncomputable def b (t : ) : := (7 - t)^(1/4)
def valid_t (t : ) : Prop :=
a t = b t
theorem LHS_to_4 : ∀ t : , (a t) ^ 4 = 4 * t - 8 := by sorry
theorem RHS_to_4 : ∀ t : , (b t) ^ 4 = 7 - t := by sorry
theorem solution : valid_t 3 := by sorry

Some files were not shown because too many files have changed in this diff Show More