Skip to content

Commit c81cb68

Browse files
committed
check forbidden thms
1 parent a6e0c5d commit c81cb68

File tree

3 files changed

+60
-250
lines changed

3 files changed

+60
-250
lines changed

relay/src/serverProcess.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,11 @@ export class GameManager {
160160

161161
let content = message.params.textDocument.text;
162162
message.params.textDocument.text =
163-
`import ${levelData.module} import GameServer.Runner Runner `+
163+
`import ${levelData.module} import GameServer.Runner Runner ` +
164164
`${JSON.stringify(gameData.name)} ${JSON.stringify(worldId)} ${levelId} ` +
165-
`${difficulty} ${JSON.stringify(inventory)} `+
166-
`:= by\n${content}\ndone`
165+
`(difficulty := ${difficulty}) ` +
166+
`(inventory := [${inventory.map(s => JSON.stringify(s)).join(',')}]) ` +
167+
`:= by\n${content}`
167168
}
168169

169170
return shiftLines(message, +1);

server/GameServer/RpcHandlers.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def getProofState (_ : Lsp.PlainGoalParams) : RequestM (RequestTask (Option Proo
236236

237237
let mut intermediateGoalCount := 0
238238

239+
-- TODO(Alex): for some reason, the client expects an empty tactic in the beginning
239240
let positionsWithSource : Array (String.Pos × String) := Id.run do
240241
let mut res := #[]
241242
for i in [0:text.positions.size] do

server/GameServer/Runner.lean

Lines changed: 55 additions & 247 deletions
Original file line numberDiff line numberDiff line change
@@ -6,141 +6,23 @@ import GameServer.Tactic.LetIntros
66

77
open 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
/--
13012
Find all tactics in syntax object that are forbidden according to a
13113
set `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

Comments
 (0)