|
|
|
@ -1,32 +1,28 @@
|
|
|
|
import Lean
|
|
|
|
import Lean
|
|
|
|
|
|
|
|
|
|
|
|
import GameServer.EnvExtensions
|
|
|
|
import GameServer.EnvExtensions
|
|
|
|
|
|
|
|
|
|
|
|
open Lean Meta
|
|
|
|
open Lean Meta Elab Command
|
|
|
|
|
|
|
|
|
|
|
|
set_option autoImplicit false
|
|
|
|
set_option autoImplicit false
|
|
|
|
|
|
|
|
|
|
|
|
/-! ## Easy metadata -/
|
|
|
|
/-! # Game metadata -/
|
|
|
|
|
|
|
|
|
|
|
|
section metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
open Lean Meta Elab Command Term
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Create a game with the given identifier as name. -/
|
|
|
|
/-- Switch to the specified `Game` (and create it if non-existent). Example: `Game "NNG"` -/
|
|
|
|
elab "Game" n:str : command => do
|
|
|
|
elab "Game" n:str : command => do
|
|
|
|
let name := n.getString
|
|
|
|
let name := n.getString
|
|
|
|
setCurGameId name
|
|
|
|
setCurGameId name
|
|
|
|
if (← getGame? name).isNone then
|
|
|
|
if (← getGame? name).isNone then
|
|
|
|
insertGame name {name}
|
|
|
|
insertGame name {name}
|
|
|
|
|
|
|
|
|
|
|
|
/-- Create a World -/
|
|
|
|
/-- Create a new world in the active game. Example: `World "Addition"` -/
|
|
|
|
elab "World" n:str : command => do
|
|
|
|
elab "World" n:str : command => do
|
|
|
|
let name := n.getString
|
|
|
|
let name := n.getString
|
|
|
|
setCurWorldId name
|
|
|
|
setCurWorldId name
|
|
|
|
if ¬ (← getCurGame).worlds.nodes.contains name then
|
|
|
|
if ¬ (← getCurGame).worlds.nodes.contains name then
|
|
|
|
addWorld {name}
|
|
|
|
addWorld {name}
|
|
|
|
|
|
|
|
|
|
|
|
/-- Define the current level number. -/
|
|
|
|
/-- Define the current level number. Levels inside a world must be
|
|
|
|
|
|
|
|
numbered consecutive starting with `1`. Example: `Level 1` -/
|
|
|
|
elab "Level" level:num : command => do
|
|
|
|
elab "Level" level:num : command => do
|
|
|
|
let level := level.getNat
|
|
|
|
let level := level.getNat
|
|
|
|
setCurLevelIdx level
|
|
|
|
setCurLevelIdx level
|
|
|
|
@ -46,99 +42,6 @@ elab "Introduction" t:str : command => do
|
|
|
|
| .World => modifyCurWorld fun world => pure {world with introduction := t.getString}
|
|
|
|
| .World => modifyCurWorld fun world => pure {world with introduction := t.getString}
|
|
|
|
| .Game => modifyCurGame fun game => pure {game with introduction := t.getString}
|
|
|
|
| .Game => modifyCurGame fun game => pure {game with introduction := t.getString}
|
|
|
|
|
|
|
|
|
|
|
|
-- TODO: Instead of this, it would be nice to have a proper syntax parser that enables
|
|
|
|
|
|
|
|
-- us highlighting on the client side.
|
|
|
|
|
|
|
|
partial def reprintCore : Syntax → Option Format
|
|
|
|
|
|
|
|
| Syntax.missing => none
|
|
|
|
|
|
|
|
| Syntax.atom _ val => val.trim
|
|
|
|
|
|
|
|
| Syntax.ident _ rawVal _ _ => rawVal.toString
|
|
|
|
|
|
|
|
| Syntax.node _ kind args =>
|
|
|
|
|
|
|
|
match args.toList.filterMap reprintCore with
|
|
|
|
|
|
|
|
| [] => none
|
|
|
|
|
|
|
|
| [arg] => arg
|
|
|
|
|
|
|
|
| args => Format.group <| Format.nest 2 <| Format.joinSep args " "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reprint (stx : Syntax) : Format :=
|
|
|
|
|
|
|
|
reprintCore stx |>.getD ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Remove any spaces at the beginning of a new line -/
|
|
|
|
|
|
|
|
partial def removeIndentation (s : String) : String :=
|
|
|
|
|
|
|
|
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
|
|
|
|
|
|
|
let c := s.get i
|
|
|
|
|
|
|
|
let i := s.next i
|
|
|
|
|
|
|
|
if s.atEnd i then
|
|
|
|
|
|
|
|
acc.push c
|
|
|
|
|
|
|
|
else if removeSpaces && c == ' ' then
|
|
|
|
|
|
|
|
loop i acc (removeSpaces := true)
|
|
|
|
|
|
|
|
else if c == '\n' then
|
|
|
|
|
|
|
|
loop i (acc.push c) (removeSpaces := true)
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
loop i (acc.push c)
|
|
|
|
|
|
|
|
loop ⟨0⟩ ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
|
|
|
|
|
|
|
|
see hints. The tactic does not affect the goal state. -/
|
|
|
|
|
|
|
|
elab "Hint" args:hintArg* msg:interpolatedStr(term) : tactic => do
|
|
|
|
|
|
|
|
let mut strict := false
|
|
|
|
|
|
|
|
let mut hidden := false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- remove spaces at the beginngng of new lines
|
|
|
|
|
|
|
|
let msg := TSyntax.mk $ msg.raw.setArgs $ ← msg.raw.getArgs.mapM fun m => do
|
|
|
|
|
|
|
|
match m with
|
|
|
|
|
|
|
|
| Syntax.node info k args =>
|
|
|
|
|
|
|
|
logInfo k
|
|
|
|
|
|
|
|
if k == interpolatedStrLitKind && args.size == 1 then
|
|
|
|
|
|
|
|
match args.get! 0 with
|
|
|
|
|
|
|
|
| (Syntax.atom info' val) =>
|
|
|
|
|
|
|
|
let val := removeIndentation val
|
|
|
|
|
|
|
|
return Syntax.node info k #[Syntax.atom info' val]
|
|
|
|
|
|
|
|
| _ => return m
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
return m
|
|
|
|
|
|
|
|
| _ => return m
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for arg in args do
|
|
|
|
|
|
|
|
match arg with
|
|
|
|
|
|
|
|
| `(hintArg| (strict := true)) => strict := true
|
|
|
|
|
|
|
|
| `(hintArg| (strict := false)) => strict := false
|
|
|
|
|
|
|
|
| `(hintArg| (hidden := true)) => hidden := true
|
|
|
|
|
|
|
|
| `(hintArg| (hidden := false)) => hidden := false
|
|
|
|
|
|
|
|
| _ => throwUnsupportedSyntax
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let goal ← Tactic.getMainGoal
|
|
|
|
|
|
|
|
goal.withContext do
|
|
|
|
|
|
|
|
-- We construct an expression that can produce the hint text. The difficulty is that we
|
|
|
|
|
|
|
|
-- want the text to possibly contain quotation of the local variables which might have been
|
|
|
|
|
|
|
|
-- named differently by the player.
|
|
|
|
|
|
|
|
let varsName := `vars
|
|
|
|
|
|
|
|
let text ← withLocalDeclD varsName (mkApp (mkConst ``Array [levelZero]) (mkConst ``Expr)) fun vars => do
|
|
|
|
|
|
|
|
let mut text ← `(m! $msg)
|
|
|
|
|
|
|
|
let goalDecl ← goal.getDecl
|
|
|
|
|
|
|
|
let decls := goalDecl.lctx.decls.toArray.filterMap id
|
|
|
|
|
|
|
|
for i in [:decls.size] do
|
|
|
|
|
|
|
|
text ← `(let $(mkIdent decls[i]!.userName) := $(mkIdent varsName)[$(quote i)]!; $text)
|
|
|
|
|
|
|
|
return ← mkLambdaFVars #[vars] $ ← elabTermAndSynthesize text none
|
|
|
|
|
|
|
|
let textmvar ← mkFreshExprMVar none
|
|
|
|
|
|
|
|
guard $ ← isDefEq textmvar text -- Store the text in a mvar.
|
|
|
|
|
|
|
|
-- The information about the hint is logged as a message using `logInfo` to transfer it to the
|
|
|
|
|
|
|
|
-- `Statement` command defined below:
|
|
|
|
|
|
|
|
logInfo $
|
|
|
|
|
|
|
|
.tagged `Hint $
|
|
|
|
|
|
|
|
.nest (if strict then 1 else 0) $
|
|
|
|
|
|
|
|
.nest (if hidden then 1 else 0) $
|
|
|
|
|
|
|
|
.compose (.ofGoal textmvar.mvarId!) (.ofGoal goal)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- This tactic allows us to execute an alternative sequence of tactics, but without affecting the
|
|
|
|
|
|
|
|
proof state. We use it to define Hints for alternative proof methods or dead ends. -/
|
|
|
|
|
|
|
|
elab "Branch" t:tacticSeq : tactic => do
|
|
|
|
|
|
|
|
let b ← saveState
|
|
|
|
|
|
|
|
Tactic.evalTactic t
|
|
|
|
|
|
|
|
let msgs ← Core.getMessageLog
|
|
|
|
|
|
|
|
b.restore
|
|
|
|
|
|
|
|
Core.setMessageLog msgs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Define the conclusion of the current game or current level if some
|
|
|
|
/-- Define the conclusion of the current game or current level if some
|
|
|
|
building a level. -/
|
|
|
|
building a level. -/
|
|
|
|
elab "Conclusion" t:str : command => do
|
|
|
|
elab "Conclusion" t:str : command => do
|
|
|
|
@ -147,20 +50,14 @@ elab "Conclusion" t:str : command => do
|
|
|
|
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
|
|
|
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
|
|
|
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
|
|
|
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
|
|
|
|
|
|
|
|
|
|
|
-- /-- Print current game for debugging purposes. -/
|
|
|
|
/-! ## World Paths -/
|
|
|
|
-- elab "PrintCurGame" : command => do
|
|
|
|
|
|
|
|
-- logInfo (toJson (← getCurGame))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Print current level for debugging purposes. -/
|
|
|
|
|
|
|
|
elab "PrintCurLevel" : command => do
|
|
|
|
|
|
|
|
logInfo (repr (← getCurLevel))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- /-- Print levels for debugging purposes. -/
|
|
|
|
|
|
|
|
elab "PrintLevels" : command => do
|
|
|
|
|
|
|
|
logInfo $ repr $ (← getCurWorld).levels.toArray
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- The worlds of a game are joint by paths. These are defined with the syntax
|
|
|
|
|
|
|
|
`Path World₁ → World₂ → World₃`. -/
|
|
|
|
def Parser.path := Parser.sepBy1Indent Parser.ident "→"
|
|
|
|
def Parser.path := Parser.sepBy1Indent Parser.ident "→"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- The worlds of a game are joint by paths. These are defined with the syntax
|
|
|
|
|
|
|
|
`Path World₁ → World₂ → World₃`. -/
|
|
|
|
elab "Path" s:Parser.path : command => do
|
|
|
|
elab "Path" s:Parser.path : command => do
|
|
|
|
let mut last : Option Name := none
|
|
|
|
let mut last : Option Name := none
|
|
|
|
for stx in s.raw.getArgs.getEvenElems do
|
|
|
|
for stx in s.raw.getArgs.getEvenElems do
|
|
|
|
@ -172,71 +69,21 @@ elab "Path" s:Parser.path : command => do
|
|
|
|
pure {game with worlds := {game.worlds with edges := game.worlds.edges.push (l, stx.getId)}}
|
|
|
|
pure {game with worlds := {game.worlds with edges := game.worlds.edges.push (l, stx.getId)}}
|
|
|
|
last := some stx.getId
|
|
|
|
last := some stx.getId
|
|
|
|
|
|
|
|
|
|
|
|
end metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-! ## Hints -/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
open Lean Meta Elab Command Term
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- From a term `s` and a list of pairs `(i, t) ; Ident × Term`, create the syntax
|
|
|
|
|
|
|
|
where `s` is preceded with universal quantifiers `∀ i : t`. -/
|
|
|
|
|
|
|
|
def mkGoalSyntax (s : Term) : List (Ident × Term) → MacroM Term
|
|
|
|
|
|
|
|
| (n, t)::tail => do return (← `(∀ $n : $t, $(← mkGoalSyntax s tail)))
|
|
|
|
|
|
|
|
| [] => return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- def elabHint (hidden : Bool) (binders : TSyntaxArray `Lean.Parser.Term.bracketedBinder)
|
|
|
|
|
|
|
|
-- (goal : TSyntax `term) (msg : TSyntax `interpolatedStrKind) :=
|
|
|
|
|
|
|
|
-- liftTermElabM do withOptions (fun options => options.setBool `linter.unusedVariables false) do
|
|
|
|
|
|
|
|
-- let (g, decls) ← elabBinders binders fun xs => do
|
|
|
|
|
|
|
|
-- let g ← mkForallFVars xs $ ← elabTermAndSynthesize goal none
|
|
|
|
|
|
|
|
-- synthesizeSyntheticMVarsNoPostponing false
|
|
|
|
|
|
|
|
-- return (← instantiateMVars g, ← xs.mapM (fun x => x.fvarId!.getDecl))
|
|
|
|
|
|
|
|
-- let varsName := `vars
|
|
|
|
|
|
|
|
-- let msg ← withLocalDeclD varsName (mkApp (mkConst ``Array [levelZero]) (mkConst ``Expr)) fun vars => do
|
|
|
|
|
|
|
|
-- let mut msg ← `(m! $msg)
|
|
|
|
|
|
|
|
-- for i in [:decls.size] do
|
|
|
|
|
|
|
|
-- msg ← `(let $(mkIdent decls[i]!.userName) := $(mkIdent varsName)[$(quote i)]!; $msg)
|
|
|
|
|
|
|
|
-- return ← mkLambdaFVars #[vars] $ ← elabTermAndSynthesize msg none
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- if g.hasMVar then throwError m!"Goal contains metavariables: {g}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- modifyCurLevel fun level => pure {level with hints := level.hints.push {
|
|
|
|
|
|
|
|
-- goal := g,
|
|
|
|
|
|
|
|
-- intros := decls.size,
|
|
|
|
|
|
|
|
-- hidden := hidden,
|
|
|
|
|
|
|
|
-- text := msg }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare a hint. This version doesn't prevent the unused linter variable from running. -/
|
|
|
|
|
|
|
|
local elab "Hint'" binders:bracketedBinder* ":" goal:term "=>" msg:interpolatedStr(term) : command =>
|
|
|
|
|
|
|
|
-- elabHint false binders goal msg
|
|
|
|
|
|
|
|
pure ()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/--
|
|
|
|
|
|
|
|
Declare a hint. This version doesn't prevent the unused linter variable from running.
|
|
|
|
|
|
|
|
A hidden hint is only displayed if explicitly requested by the user.
|
|
|
|
|
|
|
|
-/
|
|
|
|
|
|
|
|
local elab "HiddenHint'" binders:bracketedBinder* ":" goal:term "=>" msg:interpolatedStr(term) : command => do
|
|
|
|
|
|
|
|
-- elabHint true binders goal msg
|
|
|
|
|
|
|
|
pure ()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare a hint in reaction to a given tactic state in the current level. -/
|
|
|
|
|
|
|
|
macro "Hint" decls:bracketedBinder* ":" goal:term "=>" msg:interpolatedStr(term) : command => do
|
|
|
|
|
|
|
|
`(set_option linter.unusedVariables false in Hint' $decls* : $goal => $msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare a hidden hint in reaction to a given tactic state in the current level. -/
|
|
|
|
/-! # Inventory
|
|
|
|
macro "HiddenHint" decls:bracketedBinder* ":" goal:term "=>" msg:interpolatedStr(term) : command => do
|
|
|
|
|
|
|
|
`(set_option linter.unusedVariables false in HiddenHint' $decls* : $goal => $msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The inventory contains docs for tactics, lemmas, and definitions. These are all disabled
|
|
|
|
|
|
|
|
in the first level and get enabled during the game.
|
|
|
|
|
|
|
|
-/
|
|
|
|
|
|
|
|
|
|
|
|
/-! ## Inventory -/
|
|
|
|
/-! ## Doc entries -/
|
|
|
|
|
|
|
|
|
|
|
|
/-- Throw an error if inventory doc does not exist -/
|
|
|
|
/-- Throw an error if inventory doc does not exist -/
|
|
|
|
def checkInventoryDoc (type : InventoryType) (name : Name) : CommandElabM Unit := do
|
|
|
|
def checkInventoryDoc (type : InventoryType) (name : Name) : CommandElabM Unit := do
|
|
|
|
let some _ := (inventoryDocExt.getState (← getEnv)).find?
|
|
|
|
let some _ := (inventoryDocExt.getState (← getEnv)).find?
|
|
|
|
(fun x => x.name == name && x.type == type)
|
|
|
|
(fun x => x.name == name && x.type == type)
|
|
|
|
| throwError "Missing {type} Documentation: {name}"
|
|
|
|
| throwError "Missing {type} Documentation: {name} (add `{type}Doc {name}` in your game's docs section)"
|
|
|
|
|
|
|
|
|
|
|
|
/-! ### Tactics -/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Documentation entry of a tactic. Example:
|
|
|
|
/-- Documentation entry of a tactic. Example:
|
|
|
|
|
|
|
|
|
|
|
|
@ -255,26 +102,25 @@ elab "TacticDoc" name:ident content:str : command =>
|
|
|
|
displayName := name.getId.toString,
|
|
|
|
displayName := name.getId.toString,
|
|
|
|
content := content.getString })
|
|
|
|
content := content.getString })
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare tactics that are introduced by this level. -/
|
|
|
|
/-- Documentation entry of a lemma. Example:
|
|
|
|
elab "NewTactic" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
for name in names do checkInventoryDoc .Tactic name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with tactics := {level.tactics with new := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare tactics that are temporarily disabled in this level.
|
|
|
|
|
|
|
|
This is ignored if `OnlyTactic` is set. -/
|
|
|
|
|
|
|
|
elab "DisabledTactic" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
-- for name in names do checkInventoryDoc .Tactic name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with tactics := {level.tactics with disabled := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Temporarily disable all tactics except the ones declared here -/
|
|
|
|
```
|
|
|
|
elab "OnlyTactic" args:ident* : command => do
|
|
|
|
LemmaDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
```
|
|
|
|
for name in names do checkInventoryDoc .Tactic name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with tactics := {level.tactics with only := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-! ### Definitions -/
|
|
|
|
* The first identifier is used in the commands `[New/Only/Disabled]Lemma`.
|
|
|
|
|
|
|
|
It is preferably the true name of the lemma. However, this is not required.
|
|
|
|
|
|
|
|
* The string following `as` is the displayed name (in the Inventory).
|
|
|
|
|
|
|
|
* The identifier after `in` is the category to group lemmas by (in the Inventory).
|
|
|
|
|
|
|
|
* The description is a string supporting Markdown.
|
|
|
|
|
|
|
|
-/
|
|
|
|
|
|
|
|
elab "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str : command =>
|
|
|
|
|
|
|
|
modifyEnv (inventoryDocExt.addEntry · {
|
|
|
|
|
|
|
|
name := name.getId,
|
|
|
|
|
|
|
|
type := .Lemma
|
|
|
|
|
|
|
|
displayName := displayName.getString,
|
|
|
|
|
|
|
|
category := category.getString,
|
|
|
|
|
|
|
|
content := content.getString })
|
|
|
|
|
|
|
|
|
|
|
|
/-- Documentation entry of a definition. Example:
|
|
|
|
/-- Documentation entry of a definition. Example:
|
|
|
|
|
|
|
|
|
|
|
|
@ -295,18 +141,58 @@ elab "DefinitionDoc" name:ident "as" displayName:str content:str : command =>
|
|
|
|
displayName := displayName.getString,
|
|
|
|
displayName := displayName.getString,
|
|
|
|
content := content.getString })
|
|
|
|
content := content.getString })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-! ## Add inventory items -/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare tactics that are introduced by this level. -/
|
|
|
|
|
|
|
|
elab "NewTactic" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
for name in names do checkInventoryDoc .Tactic name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with tactics := {level.tactics with new := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare lemmas that are introduced by this level. -/
|
|
|
|
|
|
|
|
elab "NewLemma" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
for name in names do checkInventoryDoc .Lemma name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with lemmas := {level.lemmas with new := names}}
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare definitions that are introduced by this level. -/
|
|
|
|
/-- Declare definitions that are introduced by this level. -/
|
|
|
|
elab "NewDefinition" args:ident* : command => do
|
|
|
|
elab "NewDefinition" args:ident* : command => do
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
for name in names do checkInventoryDoc .Definition name
|
|
|
|
for name in names do checkInventoryDoc .Definition name
|
|
|
|
modifyCurLevel fun level => pure {level with definitions := {level.definitions with new := names}}
|
|
|
|
modifyCurLevel fun level => pure {level with definitions := {level.definitions with new := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare tactics that are temporarily disabled in this level.
|
|
|
|
|
|
|
|
This is ignored if `OnlyTactic` is set. -/
|
|
|
|
|
|
|
|
elab "DisabledTactic" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
-- for name in names do checkInventoryDoc .Tactic name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with tactics := {level.tactics with disabled := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare lemmas that are temporarily disabled in this level.
|
|
|
|
|
|
|
|
This is ignored if `OnlyLemma` is set. -/
|
|
|
|
|
|
|
|
elab "DisabledLemma" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
-- for name in names do checkInventoryDoc .Lemma name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with lemmas := {level.lemmas with disabled := names}}
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare definitions that are temporarily disabled in this level -/
|
|
|
|
/-- Declare definitions that are temporarily disabled in this level -/
|
|
|
|
elab "DisabledDefinition" args:ident* : command => do
|
|
|
|
elab "DisabledDefinition" args:ident* : command => do
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
-- for name in names do checkInventoryDoc .Definition name
|
|
|
|
-- for name in names do checkInventoryDoc .Definition name
|
|
|
|
modifyCurLevel fun level => pure {level with definitions := {level.definitions with disabled := names}}
|
|
|
|
modifyCurLevel fun level => pure {level with definitions := {level.definitions with disabled := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Temporarily disable all tactics except the ones declared here -/
|
|
|
|
|
|
|
|
elab "OnlyTactic" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
for name in names do checkInventoryDoc .Tactic name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with tactics := {level.tactics with only := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Temporarily disable all lemmas except the ones declared here -/
|
|
|
|
|
|
|
|
elab "OnlyLemma" args:ident* : command => do
|
|
|
|
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
|
|
|
|
for name in names do checkInventoryDoc .Lemma name
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with lemmas := {level.lemmas with only := names}}
|
|
|
|
|
|
|
|
|
|
|
|
/-- Temporarily disable all definitions except the ones declared here.
|
|
|
|
/-- Temporarily disable all definitions except the ones declared here.
|
|
|
|
This is ignored if `OnlyDefinition` is set. -/
|
|
|
|
This is ignored if `OnlyDefinition` is set. -/
|
|
|
|
elab "OnlyDefinition" args:ident* : command => do
|
|
|
|
elab "OnlyDefinition" args:ident* : command => do
|
|
|
|
@ -314,16 +200,32 @@ elab "OnlyDefinition" args:ident* : command => do
|
|
|
|
for name in names do checkInventoryDoc .Definition name
|
|
|
|
for name in names do checkInventoryDoc .Definition name
|
|
|
|
modifyCurLevel fun level => pure {level with definitions := {level.definitions with only := names}}
|
|
|
|
modifyCurLevel fun level => pure {level with definitions := {level.definitions with only := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Define which tab of Lemmas is opened by default. Usage: `LemmaTab "Nat"`.
|
|
|
|
|
|
|
|
If omitted, the current tab will remain open. -/
|
|
|
|
|
|
|
|
elab "LemmaTab" category:str : command =>
|
|
|
|
|
|
|
|
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-! ### Lemmas -/
|
|
|
|
/-! # Exercise Statement -/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- TODO: Instead of this, it would be nice to have a proper syntax parser that enables
|
|
|
|
|
|
|
|
-- us highlighting on the client side.
|
|
|
|
|
|
|
|
partial def reprintCore : Syntax → Option Format
|
|
|
|
|
|
|
|
| Syntax.missing => none
|
|
|
|
|
|
|
|
| Syntax.atom _ val => val.trim
|
|
|
|
|
|
|
|
| Syntax.ident _ rawVal _ _ => rawVal.toString
|
|
|
|
|
|
|
|
| Syntax.node _ _ args =>
|
|
|
|
|
|
|
|
match args.toList.filterMap reprintCore with
|
|
|
|
|
|
|
|
| [] => none
|
|
|
|
|
|
|
|
| [arg] => arg
|
|
|
|
|
|
|
|
| args => Format.group <| Format.nest 2 <| Format.joinSep args " "
|
|
|
|
|
|
|
|
|
|
|
|
/-- Define the statement of the current level.
|
|
|
|
/-- `reprint` is used to display the Lean-statement to the user-/
|
|
|
|
|
|
|
|
def reprint (stx : Syntax) : Format :=
|
|
|
|
|
|
|
|
reprintCore stx |>.getD ""
|
|
|
|
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
/-- Define the statement of the current level. -/
|
|
|
|
- ident: (Optional) The name of the statemtent.
|
|
|
|
|
|
|
|
- descr: (Optional) The human-readable version of the lemma as string. Accepts Markdown and Mathjax.
|
|
|
|
|
|
|
|
-/
|
|
|
|
|
|
|
|
elab "Statement" statementName:ident ? descr:str ? sig:declSig val:declVal : command => do
|
|
|
|
elab "Statement" statementName:ident ? descr:str ? sig:declSig val:declVal : command => do
|
|
|
|
|
|
|
|
|
|
|
|
-- Check that the statement name is a lemma in the doc
|
|
|
|
-- Check that the statement name is a lemma in the doc
|
|
|
|
@ -389,71 +291,118 @@ elab "Statement" statementName:ident ? descr:str ? sig:declSig val:declVal : com
|
|
|
|
} -- Format.pretty <| format thmStatement.raw }
|
|
|
|
} -- Format.pretty <| format thmStatement.raw }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Documentation entry of a lemma. Example:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
/-! # Hints -/
|
|
|
|
LemmaDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* The first identifier is used in the commands `[New/Only/Disabled]Lemma`.
|
|
|
|
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
|
|
|
It is preferably the true name of the lemma. However, this is not required.
|
|
|
|
|
|
|
|
* The string following `as` is the displayed name (in the Inventory).
|
|
|
|
/-- Remove any spaces at the beginning of a new line -/
|
|
|
|
* The identifier after `in` is the category to group lemmas by (in the Inventory).
|
|
|
|
partial def removeIndentation (s : String) : String :=
|
|
|
|
* The description is a string supporting Markdown.
|
|
|
|
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
|
|
|
|
|
|
|
let c := s.get i
|
|
|
|
|
|
|
|
let i := s.next i
|
|
|
|
|
|
|
|
if s.atEnd i then
|
|
|
|
|
|
|
|
acc.push c
|
|
|
|
|
|
|
|
else if removeSpaces && c == ' ' then
|
|
|
|
|
|
|
|
loop i acc (removeSpaces := true)
|
|
|
|
|
|
|
|
else if c == '\n' then
|
|
|
|
|
|
|
|
loop i (acc.push c) (removeSpaces := true)
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
loop i (acc.push c)
|
|
|
|
|
|
|
|
loop ⟨0⟩ ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
|
|
|
|
|
|
|
|
see hints. The tactic does not affect the goal state.
|
|
|
|
-/
|
|
|
|
-/
|
|
|
|
elab "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str : command =>
|
|
|
|
elab "Hint" args:hintArg* msg:interpolatedStr(term) : tactic => do
|
|
|
|
modifyEnv (inventoryDocExt.addEntry · {
|
|
|
|
let mut strict := false
|
|
|
|
name := name.getId,
|
|
|
|
let mut hidden := false
|
|
|
|
type := .Lemma
|
|
|
|
|
|
|
|
displayName := displayName.getString,
|
|
|
|
|
|
|
|
category := category.getString,
|
|
|
|
|
|
|
|
content := content.getString })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Define which tab of Lemmas is opened by default. Usage: `LemmaTab "Nat"`.
|
|
|
|
-- remove spaces at the beginngng of new lines
|
|
|
|
|
|
|
|
let msg := TSyntax.mk $ msg.raw.setArgs $ ← msg.raw.getArgs.mapM fun m => do
|
|
|
|
|
|
|
|
match m with
|
|
|
|
|
|
|
|
| Syntax.node info k args =>
|
|
|
|
|
|
|
|
if k == interpolatedStrLitKind && args.size == 1 then
|
|
|
|
|
|
|
|
match args.get! 0 with
|
|
|
|
|
|
|
|
| (Syntax.atom info' val) =>
|
|
|
|
|
|
|
|
let val := removeIndentation val
|
|
|
|
|
|
|
|
return Syntax.node info k #[Syntax.atom info' val]
|
|
|
|
|
|
|
|
| _ => return m
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
return m
|
|
|
|
|
|
|
|
| _ => return m
|
|
|
|
|
|
|
|
|
|
|
|
If omitted, the first tab will be open by default. -/
|
|
|
|
for arg in args do
|
|
|
|
elab "LemmaTab" category:str : command =>
|
|
|
|
match arg with
|
|
|
|
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
|
|
|
| `(hintArg| (strict := true)) => strict := true
|
|
|
|
|
|
|
|
| `(hintArg| (strict := false)) => strict := false
|
|
|
|
|
|
|
|
| `(hintArg| (hidden := true)) => hidden := true
|
|
|
|
|
|
|
|
| `(hintArg| (hidden := false)) => hidden := false
|
|
|
|
|
|
|
|
| _ => throwUnsupportedSyntax
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare lemmas that are introduced by this level. -/
|
|
|
|
let goal ← Tactic.getMainGoal
|
|
|
|
elab "NewLemma" args:ident* : command => do
|
|
|
|
goal.withContext do
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
-- We construct an expression that can produce the hint text. The difficulty is that we
|
|
|
|
for name in names do checkInventoryDoc .Lemma name
|
|
|
|
-- want the text to possibly contain quotation of the local variables which might have been
|
|
|
|
modifyCurLevel fun level => pure {level with lemmas := {level.lemmas with new := names}}
|
|
|
|
-- named differently by the player.
|
|
|
|
|
|
|
|
let varsName := `vars
|
|
|
|
|
|
|
|
let text ← withLocalDeclD varsName (mkApp (mkConst ``Array [levelZero]) (mkConst ``Expr)) fun vars => do
|
|
|
|
|
|
|
|
let mut text ← `(m! $msg)
|
|
|
|
|
|
|
|
let goalDecl ← goal.getDecl
|
|
|
|
|
|
|
|
let decls := goalDecl.lctx.decls.toArray.filterMap id
|
|
|
|
|
|
|
|
for i in [:decls.size] do
|
|
|
|
|
|
|
|
text ← `(let $(mkIdent decls[i]!.userName) := $(mkIdent varsName)[$(quote i)]!; $text)
|
|
|
|
|
|
|
|
return ← mkLambdaFVars #[vars] $ ← Term.elabTermAndSynthesize text none
|
|
|
|
|
|
|
|
let textmvar ← mkFreshExprMVar none
|
|
|
|
|
|
|
|
guard $ ← isDefEq textmvar text -- Store the text in a mvar.
|
|
|
|
|
|
|
|
-- The information about the hint is logged as a message using `logInfo` to transfer it to the
|
|
|
|
|
|
|
|
-- `Statement` command:
|
|
|
|
|
|
|
|
logInfo $
|
|
|
|
|
|
|
|
.tagged `Hint $
|
|
|
|
|
|
|
|
.nest (if strict then 1 else 0) $
|
|
|
|
|
|
|
|
.nest (if hidden then 1 else 0) $
|
|
|
|
|
|
|
|
.compose (.ofGoal textmvar.mvarId!) (.ofGoal goal)
|
|
|
|
|
|
|
|
|
|
|
|
/-- Declare lemmas that are temporarily disabled in this level.
|
|
|
|
/-- This tactic allows us to execute an alternative sequence of tactics, but without affecting the
|
|
|
|
This is ignored if `OnlyLemma` is set. -/
|
|
|
|
proof state. We use it to define Hints for alternative proof methods or dead ends. -/
|
|
|
|
elab "DisabledLemma" args:ident* : command => do
|
|
|
|
elab "Branch" t:tacticSeq : tactic => do
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
let b ← saveState
|
|
|
|
-- for name in names do checkInventoryDoc .Lemma name
|
|
|
|
Tactic.evalTactic t
|
|
|
|
modifyCurLevel fun level => pure {level with lemmas := {level.lemmas with disabled := names}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Temporarily disable all lemmas except the ones declared here -/
|
|
|
|
-- Show an info whether the branch proofs all remaining goals.
|
|
|
|
elab "OnlyLemma" args:ident* : command => do
|
|
|
|
let gs ← Tactic.getUnsolvedGoals
|
|
|
|
let names := args.map (·.getId)
|
|
|
|
if gs.isEmpty then
|
|
|
|
for name in names do checkInventoryDoc .Lemma name
|
|
|
|
logInfo "This branch finishes the proof."
|
|
|
|
modifyCurLevel fun level => pure {level with lemmas := {level.lemmas with only := names}}
|
|
|
|
else
|
|
|
|
|
|
|
|
logInfo "This branch leaves open goals."
|
|
|
|
|
|
|
|
|
|
|
|
/-! ## Make Game -/
|
|
|
|
let msgs ← Core.getMessageLog
|
|
|
|
|
|
|
|
b.restore
|
|
|
|
|
|
|
|
Core.setMessageLog msgs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-! # Make Game -/
|
|
|
|
|
|
|
|
|
|
|
|
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
|
|
|
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
|
|
|
| .Tactic => level.tactics
|
|
|
|
| .Tactic => level.tactics
|
|
|
|
| .Definition => level.definitions
|
|
|
|
| .Definition => level.definitions
|
|
|
|
| .Lemma => level.lemmas
|
|
|
|
| .Lemma => level.lemmas
|
|
|
|
|
|
|
|
|
|
|
|
def GameLevel.setComputedInventory (level : GameLevel) : InventoryType → Array ComputedInventoryItem → GameLevel
|
|
|
|
def GameLevel.setComputedInventory (level : GameLevel) :
|
|
|
|
|
|
|
|
InventoryType → Array ComputedInventoryItem → GameLevel
|
|
|
|
| .Tactic, v => {level with tactics := {level.tactics with computed := v}}
|
|
|
|
| .Tactic, v => {level with tactics := {level.tactics with computed := v}}
|
|
|
|
| .Definition, v => {level with definitions := {level.definitions with computed := v}}
|
|
|
|
| .Definition, v => {level with definitions := {level.definitions with computed := v}}
|
|
|
|
| .Lemma, v => {level with lemmas := {level.lemmas with computed := v}}
|
|
|
|
| .Lemma, v => {level with lemmas := {level.lemmas with computed := v}}
|
|
|
|
|
|
|
|
|
|
|
|
/-- Make the final Game. This command will precompute various things about the game, such as which
|
|
|
|
/-- Build the game. This command will precompute various things about the game, such as which
|
|
|
|
tactics are available in each level etc. -/
|
|
|
|
tactics are available in each level etc. -/
|
|
|
|
elab "MakeGame" : command => do
|
|
|
|
elab "MakeGame" : command => do
|
|
|
|
let game ← getCurGame
|
|
|
|
let game ← getCurGame
|
|
|
|
|
|
|
|
|
|
|
|
-- Check for loops in world graph
|
|
|
|
-- Check for loops in world graph
|
|
|
|
if game.worlds.hasLoops then
|
|
|
|
if game.worlds.hasLoops then
|
|
|
|
throwError "World graph has loops!"
|
|
|
|
throwError "World graph must not contain loops! Check your `Path` declarations."
|
|
|
|
|
|
|
|
|
|
|
|
-- Compute which inventory items are available in which level:
|
|
|
|
-- Compute which inventory items are available in which level:
|
|
|
|
for inventoryType in open InventoryType in #[Tactic, Definition, Lemma] do
|
|
|
|
for inventoryType in open InventoryType in #[Tactic, Definition, Lemma] do
|
|
|
|
@ -523,3 +472,19 @@ elab "MakeGame" : command => do
|
|
|
|
|
|
|
|
|
|
|
|
modifyLevel ⟨← getCurGameId, worldId, levelId⟩ fun level => do
|
|
|
|
modifyLevel ⟨← getCurGameId, worldId, levelId⟩ fun level => do
|
|
|
|
return level.setComputedInventory inventoryType itemsArray
|
|
|
|
return level.setComputedInventory inventoryType itemsArray
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-! # Debugging tools -/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- /-- Print current game for debugging purposes. -/
|
|
|
|
|
|
|
|
-- elab "PrintCurGame" : command => do
|
|
|
|
|
|
|
|
-- logInfo (toJson (← getCurGame))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Print current level for debugging purposes. -/
|
|
|
|
|
|
|
|
elab "PrintCurLevel" : command => do
|
|
|
|
|
|
|
|
logInfo (repr (← getCurLevel))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/-- Print levels for debugging purposes. -/
|
|
|
|
|
|
|
|
elab "PrintLevels" : command => do
|
|
|
|
|
|
|
|
logInfo $ repr $ (← getCurWorld).levels.toArray
|
|
|
|
|