@@ -6,141 +6,23 @@ import GameServer.Tactic.LetIntros
66
77open Lean Meta Elab Command
88
9- /-- Define the statement of the current level. -/
10- elab "Runner" gameId:str worldId:str levelId:num difficulty:num inventory:term val:declVal : command => do
11-
12- let game := gameId.getString
13- let world := worldId.getString
14- let level := levelId.getNat
15- let difficulty := difficulty.getNat
16- -- let inventory := inventory.getExpr
17-
18- -- extract the `tacticSeq` from `val` in order to add `let_intros` in front.
19- let tacticStx : TSyntax `Lean.Parser.Tactic.tacticSeq := match val with
20- | `(Parser.Command.declVal| := by $proof) => proof
21- | _ => panic "expected `:= by`"
22-
23- let some level ← getLevel? {game := game, world := world, level := level}
24- | panic! "Level not found"
25-
26- let thmStatement ← `(command|
27- theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx⟩)} )
28- elabCommand thmStatement
29-
30-
31-
32-
33- --TODO: Integrate the following material from the old FileWorker
34-
35- /--
36- Collection of items which are considered unlocked.
37- Tactics and theorems are mixed together.
38- -/
39- def inventory : Array String := #[]
40- /--
41- Difficulty determines whether tactics/theorems can be locked.
42- * 0: do not check
43- * 1: give warnings when locked items are used
44- * 2: give errors when locked items are used
45- -/
46- def difficulty : Nat := 0
47-
48-
49- open Lean
50- open Elab
51- open Parser
52-
53- private def mkErrorMessage (c : InputContext) (pos : String.Pos) (errorMsg : String) : Message :=
54- let pos := c.fileMap.toPosition pos
55- { fileName := c.fileName, pos := pos, data := errorMsg }
56-
57- private def mkEOI (pos : String.Pos) : Syntax :=
58- let atom := mkAtom (SourceInfo.original "" .toSubstring pos "" .toSubstring pos) ""
59- mkNode ``Command.eoi #[atom]
60-
61- partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
62- (mps : ModuleParserState) (messages : MessageLog) :
63- Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
64- let mut pos := mps.pos
65- let mut recovering := mps.recovering
66- let mut messages := messages
67- let mut stx := Syntax.missing -- will always be assigned below
68-
69- let tokens := getTokenTable pmctx.env
70-
71- let s := whitespace.run inputCtx pmctx tokens { cache := initCacheForInput inputCtx.input, pos }
72- let endOfWhitespace := s.pos
73-
74- let p := (Tactic.sepByIndentSemicolon tacticParser).fn
75- let s := p.run inputCtx pmctx tokens { cache := initCacheForInput inputCtx.input, pos }
76-
77- pos := s.pos
78- match s.errorMsg with
79- | none =>
80- stx := s.stxStack.back
81- recovering := false
82- | some errorMsg =>
83- messages := messages.add <| mkErrorMessage inputCtx s.pos (toString errorMsg)
84- recovering := true
85- stx := s.stxStack.back
86- if ¬ inputCtx.input.atEnd s.pos then
87- messages := messages.add <| mkErrorMessage inputCtx s.pos "end of input"
88- return (stx, { pos := inputCtx.input.endPos, recovering }, messages, endOfWhitespace)
89-
90- namespace GameServer.FileWorker
91-
92-
93- open Lean
94- open Lean.Server
95- open Lean.Server.FileWorker
96- open Lsp
97- open IO
98- open Snapshots
99- open JsonRpc
100-
101- def gameDir := "."
102-
103- def levelId : LevelId := {
104- game := "Game" ,
105- world := "DemoWorld" ,
106- level := 1
107- }
108-
109- /--
110- Pack the our custom `WorkerState` on top of the normal worker monad
111- `Server.FileWorker.WorkerM`.
112- -/
113- abbrev WorkerM := StateT WorkerState Server.FileWorker.WorkerM
114-
115- section Elab
116-
117- /-- Add a message. use `(severity := .warning)` to specify the severity-/
118- def addMessage (info : SourceInfo) (inputCtx : Parser.InputContext)
119- (severity := MessageSeverity.warning) (s : MessageData) :
120- Elab.Command.CommandElabM Unit := do
121- modify fun st => { st with
122- messages := st.messages.add {
123- fileName := inputCtx.fileName
124- severity := severity
125- pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0 )
126- data := s }}
1279
12810-- TODO: use HashSet for allowed tactics?
12911/--
13012Find all tactics in syntax object that are forbidden according to a
13113set `allowed` of allowed tactics.
13214-/
133- partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState : WorkerState)
134- (stx : Syntax) : Elab.Command. CommandElabM Unit := do
135- let levelInfo ← loadLevelData gameDir levelId.world levelId.level
15+ partial def findForbiddenTactics
16+ (levelId : LevelId) (inventory : List String) (difficulty : Nat) ( stx : Syntax) : CommandElabM Unit := do
17+ let levelInfo ← loadLevelData "." levelId.world levelId.level
13618 -- Parse the syntax object and look for tactics and declarations.
13719 match stx with
13820 | .missing => return ()
13921 | .node _info _kind args =>
14022 -- Go inside a node.
14123 for arg in args do
142- findForbiddenTactics inputCtx workerState arg
143- | .atom info val =>
24+ findForbiddenTactics levelId inventory difficulty arg
25+ | .atom _ val =>
14426 -- Atoms might be tactic names or other keywords.
14527 -- Note: We whitelisted known keywords because we cannot
14628 -- distinguish keywords from tactic names.
@@ -159,21 +41,21 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState :
15941 pure ()
16042 | none =>
16143 -- Tactic is not in the inventory.
162- addMessageByDifficulty info s! "The tactic '{ val} ' is not available in this game!"
44+ addMessageByDifficulty s! "The tactic '{ val} ' is not available in this game!"
16345 | some tac =>
16446 -- Tactic is introduced at some point in the game.
16547 if tac.disabled then
16648 -- Tactic is disabled in this level.
167- addMessageByDifficulty info s! "The tactic '{ val} ' is disabled in this level!"
49+ addMessageByDifficulty s! "The tactic '{ val} ' is disabled in this level!"
16850 else if tac.locked then
16951 match inventory.find? (· == val) with
17052 | none =>
17153 -- Tactic is marked as locked and not in the inventory.
172- addMessageByDifficulty info s! "You have not unlocked the tactic '{ val} ' yet!"
54+ addMessageByDifficulty s! "You have not unlocked the tactic '{ val} ' yet!"
17355 | some _ =>
17456 -- Tactic is in the inventory, allow it.
17557 pure ()
176- | .ident info _rawVal val _preresolved =>
58+ | .ident _ _rawVal val _preresolved =>
17759 -- Try to resolve the name
17860 let ns ←
17961 try resolveGlobalConst (mkIdent val)
@@ -185,143 +67,69 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState :
18567 | return ()
18668 if some n = levelInfo.statementName then
18769 -- Forbid the theorem we are proving currently
188- addMessage info inputCtx (severity := .error)
189- s! "Structural recursion: you can't use '{ n} ' to proof itself!"
70+ logErrorAt stx m! "Structural recursion: you can't use '{ n} ' to proof itself!"
19071 let theoremsAndDefs := levelInfo.lemmas ++ levelInfo.definitions
19172 match theoremsAndDefs.find? (·.name == n) with
19273 | none =>
19374 -- Theorem will never be introduced in this game
194- addMessageByDifficulty info s! "The theorem/definition '{ n} ' is not available in this game!"
75+ addMessageByDifficulty s! "The theorem/definition '{ n} ' is not available in this game!"
19576 | some thm =>
19677 -- Theorem is introduced at some point in the game.
19778 if thm.disabled then
19879 -- Theorem is disabled in this level.
199- addMessageByDifficulty info s! "The theorem/definition '{ n} ' is disabled in this level!"
80+ addMessageByDifficulty s! "The theorem/definition '{ n} ' is disabled in this level!"
20081 else if thm.locked then
20182 match inventory.find? (· == n.toString) with
20283 | none =>
20384 -- Theorem is still locked.
204- addMessageByDifficulty info s! "You have not unlocked the theorem/definition '{ n} ' yet!"
85+ addMessageByDifficulty s! "You have not unlocked the theorem/definition '{ n} ' yet!"
20586 | some _ =>
20687 -- Theorem is in the inventory, allow it.
20788 pure ()
20889
209- where addMessageByDifficulty (info : SourceInfo) (s : MessageData) :=
210- -- See `GameServer.FileWorker.WorkerState.difficulty`. Send nothing/warnings/errors
211- -- depending on difficulty.
212- let difficulty := difficulty
90+ where addMessageByDifficulty (s : MessageData) :=
91+ -- Send nothing/warnings/errors depending on difficulty.
21392 if difficulty > 0 then
214- addMessage info inputCtx (if difficulty > 1 then .error else .warning) s
93+ logAt stx s (if difficulty > 1 then .error else .warning)
21594 else pure ()
21695
217- open Elab Meta Expr in
96+ -- TODO(Alex): Use config parser?
97+ -- TODO(Alex): Ensure Runner is the last command in the file
98+ /-- Run a game level -/
99+ elab "Runner" gameId:str worldId:str levelId:num
100+ "(" &"difficulty" ":=" difficulty:num ")"
101+ "(" &"inventory" ":=" "[" inventory:str,* "]" ")"
102+ val:declVal : command => do
218103
219- def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
220- (couldBeEndSnap : Bool) (gameWorkerState : WorkerState)
221- (initParams : Lsp.InitializeParams) : IO Snapshot := do
222- -- Recognize end snap
223- if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
224- let endSnap : Snapshot := {
225- beginPos := snap.mpState.pos
226- stx := mkEOI snap.mpState.pos
227- mpState := snap.mpState
228- cmdState := snap.cmdState
229- interactiveDiags := ← withNewInteractiveDiags snap.msgLog
230- tacticCache := snap.tacticCache
231- }
232- return endSnap
233-
234- let parseResultRef ← IO.mkRef (Syntax.missing, snap.mpState)
235-
236- let cmdStateRef ← IO.mkRef snap.cmdState
237- /- The same snapshot may be executed by different tasks. So, to make sure `elabCommandTopLevel` has exclusive
238- access to the cache, we create a fresh reference here. Before this change, the
239- following `snap.tacticCache.modify` would reset the tactic post cache while another snapshot was still using it. -/
240- let tacticCacheNew ← IO.mkRef (← snap.tacticCache.get)
241- let cmdCtx : Elab.Command.Context := {
242- cmdPos := snap.endPos
243- fileName := inputCtx.fileName
244- fileMap := inputCtx.fileMap
245- tacticCache? := some tacticCacheNew
246- }
247- let (output, _) ← IO.FS.withIsolatedStreams (isolateStderr := server.stderrAsMessages.get snap.cmdState.scopes.head!.opts) <| liftM (m := BaseIO) do
248- Elab.Command.catchExceptions
249- (getResetInfoTrees *> do
250- let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
251- | panic! s! "Level not found: { inputCtx.fileName} / { GameServer.levelIdFromFileName? initParams inputCtx.fileName} "
252- let scope := level.scope
253-
254- -- use open namespaces and options as in the level file
255- Elab.Command.withScope (fun _ => scope) do
256- for od in scope.openDecls do
257- let .simple ns _ := od
258- | pure ()
259- activateScoped ns
260- activateScoped scope.currNamespace
261-
262- -- parse tactics
263- let pmctx := {
264- env := ← getEnv,
265- options := scope.opts,
266- currNamespace := scope.currNamespace,
267- openDecls := scope.openDecls }
268- let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
269- parseTactic inputCtx pmctx snap.mpState snap.msgLog
270- modify (fun s => { s with messages := msgLog })
271- parseResultRef.set (tacticStx, cmdParserState)
272-
273- -- Check for forbidden tactics
274- findForbiddenTactics inputCtx gameWorkerState tacticStx
275-
276- -- Insert invisible `skip` command to make sure we always display the initial goal
277- let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
278- -- Insert final `done` command to display unsolved goal error in the end
279- let done := Syntax.node (.synthetic cmdParserState.pos cmdParserState.pos) ``Lean.Parser.Tactic.done #[]
280- let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩)
281- let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*)
282-
283- -- Always call `let_intros` to get rid `let` statements in the goal.
284- -- This makes the experience for the user much nicer and allows for local
285- -- definitions in the exercise.
286- let cmdStx ← `(command|
287- theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx⟩)} )
288- Elab.Command.elabCommandTopLevel cmdStx)
289- cmdCtx cmdStateRef
290- let postNew := (← tacticCacheNew.get).post
291- snap.tacticCache.modify fun _ => { pre := postNew, post := {} }
292- let mut postCmdState ← cmdStateRef.get
293- if !output.isEmpty then
294- postCmdState := {
295- postCmdState with
296- messages := postCmdState.messages.add {
297- fileName := inputCtx.fileName
298- severity := MessageSeverity.information
299- pos := inputCtx.fileMap.toPosition snap.endPos
300- data := output
301- }
302- }
303-
304- let (tacticStx, cmdParserState) ← parseResultRef.get
305- if tacticStx.isMissing then throwServerError "Tactic execution went wrong. No stx found."
306-
307- let postCmdSnap : Snapshot := {
308- beginPos := tacticStx.getPos?.getD 0
309- stx := tacticStx
310- mpState := cmdParserState
311- cmdState := postCmdState
312- interactiveDiags := ← withNewInteractiveDiags postCmdState.messages
313- tacticCache := (← IO.mkRef {})
314- }
315- return postCmdSnap
316-
317- where
318- /-- Compute the current interactive diagnostics log by finding a "diff" relative to the parent
319- snapshot. We need to do this because unlike the `MessageLog` itself, interactive diags are not
320- part of the command state. -/
321- withNewInteractiveDiags (msgLog : MessageLog) : IO (PersistentArray Widget.InteractiveDiagnostic) := do
322- let newMsgCount := msgLog.msgs.size - snap.msgLog.msgs.size
323- let mut ret := snap.interactiveDiags
324- for i in List.iota newMsgCount do
325- let newMsg := msgLog.msgs.get! (msgLog.msgs.size - i)
326- ret := ret.push (← Widget.msgToInteractiveDiagnostic inputCtx.fileMap newMsg hasWidgets)
327- return ret
104+ let levelId := {game := gameId.getString, world := worldId.getString, level := levelId.getNat}
105+ let difficulty := difficulty.getNat
106+ let inventory := inventory.getElems.map (·.getString) |>.toList
107+
108+ let some level ← getLevel? levelId
109+ | logError m! "Level not found: { levelId} "
110+
111+ let scope := level.scope
112+
113+ -- extract the `tacticSeq` from `val`
114+ let tacticStx : TSyntax `Lean.Parser.Tactic.tacticSeq ←
115+ match val with
116+ | `(Parser.Command.declVal| := by $proof) => pure proof
117+ | _ => do
118+ logErrorAt val m! "expected `:= by`"
119+ throwUnsupportedSyntax
120+
121+ -- Check for forbidden tactics
122+ findForbiddenTactics levelId inventory difficulty tacticStx
123+
124+ -- use open namespaces and options as in the level file
125+ Elab.Command.withScope (fun _ => scope) do
126+ for od in scope.openDecls do
127+ let .simple ns _ := od
128+ | pure ()
129+ activateScoped ns
130+ activateScoped scope.currNamespace
131+
132+ -- Run the proof
133+ let thmStatement ← `(command|
134+ theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx⟩); done} )
135+ elabCommand thmStatement
0 commit comments