|
|
|
@ -1,7 +1,8 @@
|
|
|
|
/- This file is mostly copied from `Lean/Server/FileWorker.lean`. -/
|
|
|
|
/- This file is adapted from `Lean/Server/FileWorker.lean`. -/
|
|
|
|
import Lean.Server.FileWorker
|
|
|
|
import Lean.Server.FileWorker
|
|
|
|
import GameServer.Game
|
|
|
|
import GameServer.Game
|
|
|
|
import GameServer.ImportModules
|
|
|
|
import GameServer.ImportModules
|
|
|
|
|
|
|
|
import GameServer.SaveData
|
|
|
|
|
|
|
|
|
|
|
|
namespace MyModule
|
|
|
|
namespace MyModule
|
|
|
|
open Lean
|
|
|
|
open Lean
|
|
|
|
@ -17,7 +18,7 @@ private def mkEOI (pos : String.Pos) : Syntax :=
|
|
|
|
mkNode ``Command.eoi #[atom]
|
|
|
|
mkNode ``Command.eoi #[atom]
|
|
|
|
|
|
|
|
|
|
|
|
partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
|
|
|
partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
|
|
|
(mps : ModuleParserState) (messages : MessageLog) (couldBeEndSnap : Bool) :
|
|
|
|
(mps : ModuleParserState) (messages : MessageLog) :
|
|
|
|
Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
|
|
|
|
Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
|
|
|
|
let mut pos := mps.pos
|
|
|
|
let mut pos := mps.pos
|
|
|
|
let mut recovering := mps.recovering
|
|
|
|
let mut recovering := mps.recovering
|
|
|
|
@ -56,6 +57,20 @@ open IO
|
|
|
|
open Snapshots
|
|
|
|
open Snapshots
|
|
|
|
open JsonRpc
|
|
|
|
open JsonRpc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
structure GameWorkerState :=
|
|
|
|
|
|
|
|
inventory : Array String
|
|
|
|
|
|
|
|
/--
|
|
|
|
|
|
|
|
Check for tactics/theorems that are not unlocked.
|
|
|
|
|
|
|
|
0: no check
|
|
|
|
|
|
|
|
1: give warnings
|
|
|
|
|
|
|
|
2: give errors
|
|
|
|
|
|
|
|
-/
|
|
|
|
|
|
|
|
difficulty : Nat
|
|
|
|
|
|
|
|
levelInfo : LevelInfo
|
|
|
|
|
|
|
|
deriving ToJson, FromJson
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
abbrev GameWorkerM := StateT GameWorkerState Server.FileWorker.WorkerM
|
|
|
|
|
|
|
|
|
|
|
|
section Elab
|
|
|
|
section Elab
|
|
|
|
|
|
|
|
|
|
|
|
def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : MessageData) :
|
|
|
|
def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : MessageData) :
|
|
|
|
@ -73,29 +88,30 @@ def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : Me
|
|
|
|
/-- Find all tactics in syntax object that are forbidden according to a
|
|
|
|
/-- Find all tactics in syntax object that are forbidden according to a
|
|
|
|
set `allowed` of allowed tactics. -/
|
|
|
|
set `allowed` of allowed tactics. -/
|
|
|
|
partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|
|
|
partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|
|
|
(levelParams : Game.DidOpenLevelParams) (stx : Syntax) :
|
|
|
|
(gameWorkerState : GameWorkerState) (stx : Syntax) :
|
|
|
|
Elab.Command.CommandElabM Unit := do
|
|
|
|
Elab.Command.CommandElabM Unit := do
|
|
|
|
|
|
|
|
let levelInfo := gameWorkerState.levelInfo
|
|
|
|
match stx with
|
|
|
|
match stx with
|
|
|
|
| .missing => return ()
|
|
|
|
| .missing => return ()
|
|
|
|
| .node _info _kind args =>
|
|
|
|
| .node _info _kind args =>
|
|
|
|
for arg in args do
|
|
|
|
for arg in args do
|
|
|
|
findForbiddenTactics inputCtx levelParams arg
|
|
|
|
findForbiddenTactics inputCtx gameWorkerState arg
|
|
|
|
| .atom info val =>
|
|
|
|
| .atom info val =>
|
|
|
|
-- ignore syntax elements that do not start with a letter
|
|
|
|
-- ignore syntax elements that do not start with a letter
|
|
|
|
-- and ignore "with" keyword
|
|
|
|
-- and ignore "with" keyword
|
|
|
|
let allowed := ["with", "fun", "at", "only", "by", "to"]
|
|
|
|
let allowed := ["with", "fun", "at", "only", "by", "to"]
|
|
|
|
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
|
|
|
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
|
|
|
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
|
|
|
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
|
|
|
match levelParams.tactics.find? (·.name.toString == val) with
|
|
|
|
match levelInfo.tactics.find? (·.name.toString == val) with
|
|
|
|
| none =>
|
|
|
|
| none =>
|
|
|
|
-- Note: This case means that the tactic will never be introduced in the game.
|
|
|
|
-- Note: This case means that the tactic will never be introduced in the game.
|
|
|
|
match levelParams.inventory.find? (· == val) with
|
|
|
|
match gameWorkerState.inventory.find? (· == val) with
|
|
|
|
| none =>
|
|
|
|
| none =>
|
|
|
|
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
|
|
|
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
|
|
|
| some _ => pure () -- tactic is in the inventory, allow it.
|
|
|
|
| some _ => pure () -- tactic is in the inventory, allow it.
|
|
|
|
| some tac =>
|
|
|
|
| some tac =>
|
|
|
|
if tac.locked then
|
|
|
|
if tac.locked then
|
|
|
|
match levelParams.inventory.find? (· == val) with
|
|
|
|
match gameWorkerState.inventory.find? (· == val) with
|
|
|
|
| none =>
|
|
|
|
| none =>
|
|
|
|
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
|
|
|
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
|
|
|
| some _ => pure () -- tactic is in the inventory, allow it.
|
|
|
|
| some _ => pure () -- tactic is in the inventory, allow it.
|
|
|
|
@ -109,10 +125,10 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|
|
|
let some (.thmInfo ..) := (← getEnv).find? n
|
|
|
|
let some (.thmInfo ..) := (← getEnv).find? n
|
|
|
|
| return () -- not a theorem -> ignore
|
|
|
|
| return () -- not a theorem -> ignore
|
|
|
|
-- Forbid the theorem we are proving currently
|
|
|
|
-- Forbid the theorem we are proving currently
|
|
|
|
if n = levelParams.statementName then
|
|
|
|
if some n = levelInfo.statementName then
|
|
|
|
addErrorMessage info inputCtx s!"Structural recursion: you can't use '{n}' to proof itself!"
|
|
|
|
addErrorMessage info inputCtx s!"Structural recursion: you can't use '{n}' to proof itself!"
|
|
|
|
|
|
|
|
|
|
|
|
let lemmasAndDefs := levelParams.lemmas ++ levelParams.definitions
|
|
|
|
let lemmasAndDefs := levelInfo.lemmas ++ levelInfo.definitions
|
|
|
|
match lemmasAndDefs.find? (fun l => l.name == n) with
|
|
|
|
match lemmasAndDefs.find? (fun l => l.name == n) with
|
|
|
|
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
|
|
|
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
|
|
|
| some lem =>
|
|
|
|
| some lem =>
|
|
|
|
@ -121,7 +137,7 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|
|
|
else if lem.disabled then
|
|
|
|
else if lem.disabled then
|
|
|
|
addWarningMessage info s!"The lemma/definition '{n}' is disabled in this level!"
|
|
|
|
addWarningMessage info s!"The lemma/definition '{n}' is disabled in this level!"
|
|
|
|
where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
|
|
|
where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
|
|
|
let difficulty := levelParams.difficulty
|
|
|
|
let difficulty := gameWorkerState.difficulty
|
|
|
|
if difficulty > 0 then
|
|
|
|
if difficulty > 0 then
|
|
|
|
modify fun st => { st with
|
|
|
|
modify fun st => { st with
|
|
|
|
messages := st.messages.add {
|
|
|
|
messages := st.messages.add {
|
|
|
|
@ -137,7 +153,7 @@ where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
|
|
|
|
|
|
|
|
|
|
|
open Elab Meta Expr in
|
|
|
|
open Elab Meta Expr in
|
|
|
|
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
|
|
|
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
|
|
|
(couldBeEndSnap : Bool) (levelParams : Game.DidOpenLevelParams)
|
|
|
|
(couldBeEndSnap : Bool) (gameWorkerState : GameWorkerState)
|
|
|
|
(initParams : Lsp.InitializeParams) : IO Snapshot := do
|
|
|
|
(initParams : Lsp.InitializeParams) : IO Snapshot := do
|
|
|
|
-- Recognize end snap
|
|
|
|
-- Recognize end snap
|
|
|
|
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
|
|
|
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
|
|
|
@ -168,7 +184,7 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|
|
|
Elab.Command.catchExceptions
|
|
|
|
Elab.Command.catchExceptions
|
|
|
|
(getResetInfoTrees *> do
|
|
|
|
(getResetInfoTrees *> do
|
|
|
|
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
|
|
|
|
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
|
|
|
|
| throwError "Level not found: {inputCtx.fileName}"
|
|
|
|
| panic! s!"Level not found: {inputCtx.fileName} / {GameServer.levelIdFromFileName? initParams inputCtx.fileName}"
|
|
|
|
let scope := level.scope
|
|
|
|
let scope := level.scope
|
|
|
|
|
|
|
|
|
|
|
|
-- use open namespaces and options as in the level file
|
|
|
|
-- use open namespaces and options as in the level file
|
|
|
|
@ -186,12 +202,12 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|
|
|
currNamespace := scope.currNamespace,
|
|
|
|
currNamespace := scope.currNamespace,
|
|
|
|
openDecls := scope.openDecls }
|
|
|
|
openDecls := scope.openDecls }
|
|
|
|
let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
|
|
|
|
let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
|
|
|
|
MyModule.parseTactic inputCtx pmctx snap.mpState snap.msgLog couldBeEndSnap
|
|
|
|
MyModule.parseTactic inputCtx pmctx snap.mpState snap.msgLog
|
|
|
|
modify (fun s => { s with messages := msgLog })
|
|
|
|
modify (fun s => { s with messages := msgLog })
|
|
|
|
parseResultRef.set (tacticStx, cmdParserState)
|
|
|
|
parseResultRef.set (tacticStx, cmdParserState)
|
|
|
|
|
|
|
|
|
|
|
|
-- Check for forbidden tactics
|
|
|
|
-- Check for forbidden tactics
|
|
|
|
findForbiddenTactics inputCtx levelParams tacticStx
|
|
|
|
findForbiddenTactics inputCtx gameWorkerState tacticStx
|
|
|
|
|
|
|
|
|
|
|
|
-- Insert invisible `skip` command to make sure we always display the initial goal
|
|
|
|
-- Insert invisible `skip` command to make sure we always display the initial goal
|
|
|
|
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
|
|
|
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
|
|
|
@ -219,6 +235,7 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let (tacticStx, cmdParserState) ← parseResultRef.get
|
|
|
|
let (tacticStx, cmdParserState) ← parseResultRef.get
|
|
|
|
|
|
|
|
if tacticStx.isMissing then throwServerError "Tactic execution went wrong. No stx found."
|
|
|
|
|
|
|
|
|
|
|
|
let postCmdSnap : Snapshot := {
|
|
|
|
let postCmdSnap : Snapshot := {
|
|
|
|
beginPos := tacticStx.getPos?.getD 0
|
|
|
|
beginPos := tacticStx.getPos?.getD 0
|
|
|
|
@ -270,7 +287,7 @@ where
|
|
|
|
|
|
|
|
|
|
|
|
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
|
|
|
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
|
|
|
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
|
|
|
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
|
|
|
(levelParams : Game.DidOpenLevelParams) (initParams : Lsp.InitializeParams)
|
|
|
|
(gameWorkerState : GameWorkerState) (initParams : Lsp.InitializeParams)
|
|
|
|
: AsyncElabM (Option Snapshot) := do
|
|
|
|
: AsyncElabM (Option Snapshot) := do
|
|
|
|
cancelTk.check
|
|
|
|
cancelTk.check
|
|
|
|
let s ← get
|
|
|
|
let s ← get
|
|
|
|
@ -288,7 +305,7 @@ where
|
|
|
|
-- we can see the current goal even on an empty document
|
|
|
|
-- we can see the current goal even on an empty document
|
|
|
|
let couldBeEndSnap := s.snaps.size > 1
|
|
|
|
let couldBeEndSnap := s.snaps.size > 1
|
|
|
|
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
|
|
|
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
|
|
|
levelParams initParams
|
|
|
|
gameWorkerState initParams
|
|
|
|
set { s with snaps := s.snaps.push snap }
|
|
|
|
set { s with snaps := s.snaps.push snap }
|
|
|
|
-- TODO(MH): check for interrupt with increased precision
|
|
|
|
-- TODO(MH): check for interrupt with increased precision
|
|
|
|
cancelTk.check
|
|
|
|
cancelTk.check
|
|
|
|
@ -310,7 +327,7 @@ where
|
|
|
|
|
|
|
|
|
|
|
|
/-- Elaborates all commands after the last snap (at least the header snap is assumed to exist), emitting the diagnostics into `hOut`. -/
|
|
|
|
/-- Elaborates all commands after the last snap (at least the header snap is assumed to exist), emitting the diagnostics into `hOut`. -/
|
|
|
|
def unfoldSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
|
|
|
def unfoldSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
|
|
|
(startAfterMs : UInt32) (levelParams : Game.DidOpenLevelParams)
|
|
|
|
(startAfterMs : UInt32) (gameWorkerState : GameWorkerState)
|
|
|
|
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
|
|
|
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
|
|
|
let ctx ← read
|
|
|
|
let ctx ← read
|
|
|
|
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
|
|
|
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
|
|
|
@ -326,21 +343,15 @@ where
|
|
|
|
publishIleanInfoUpdate m ctx.hOut snaps
|
|
|
|
publishIleanInfoUpdate m ctx.hOut snaps
|
|
|
|
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
|
|
|
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
|
|
|
IO.sleep startAfterMs
|
|
|
|
IO.sleep startAfterMs
|
|
|
|
AsyncList.unfoldAsync (nextSnap ctx m cancelTk levelParams ctx.initParams) { snaps })
|
|
|
|
AsyncList.unfoldAsync (nextSnap ctx m cancelTk gameWorkerState ctx.initParams) { snaps })
|
|
|
|
|
|
|
|
|
|
|
|
end Elab
|
|
|
|
end Elab
|
|
|
|
|
|
|
|
|
|
|
|
structure GameWorkerState :=
|
|
|
|
|
|
|
|
(levelParams : Game.DidOpenLevelParams)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
abbrev GameWorkerM := StateT GameWorkerState Server.FileWorker.WorkerM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
section Updates
|
|
|
|
section Updates
|
|
|
|
|
|
|
|
|
|
|
|
/-- Given the new document, updates editable doc state. -/
|
|
|
|
/-- Given the new document, updates editable doc state. -/
|
|
|
|
def updateDocument (newMeta : DocumentMeta) : GameWorkerM Unit := do
|
|
|
|
def updateDocument (newMeta : DocumentMeta) : GameWorkerM Unit := do
|
|
|
|
let s ← get
|
|
|
|
let s ← get
|
|
|
|
let levelParams := s.levelParams
|
|
|
|
|
|
|
|
let ctx ← read
|
|
|
|
let ctx ← read
|
|
|
|
let oldDoc := (← StateT.lift get).doc
|
|
|
|
let oldDoc := (← StateT.lift get).doc
|
|
|
|
oldDoc.cancelTk.set
|
|
|
|
oldDoc.cancelTk.set
|
|
|
|
@ -382,7 +393,7 @@ section Updates
|
|
|
|
validSnaps := validSnaps.dropLast
|
|
|
|
validSnaps := validSnaps.dropLast
|
|
|
|
-- wait for a bit, giving the initial `cancelTk.check` in `nextCmdSnap` time to trigger
|
|
|
|
-- wait for a bit, giving the initial `cancelTk.check` in `nextCmdSnap` time to trigger
|
|
|
|
-- before kicking off any expensive elaboration (TODO: make expensive elaboration cancelable)
|
|
|
|
-- before kicking off any expensive elaboration (TODO: make expensive elaboration cancelable)
|
|
|
|
unfoldSnaps newMeta validSnaps.toArray cancelTk levelParams ctx
|
|
|
|
unfoldSnaps newMeta validSnaps.toArray cancelTk s ctx
|
|
|
|
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
|
|
|
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
|
|
|
StateT.lift <| modify fun st => { st with
|
|
|
|
StateT.lift <| modify fun st => { st with
|
|
|
|
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
|
|
|
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
|
|
|
@ -397,24 +408,23 @@ section Initialization
|
|
|
|
fileMap := default
|
|
|
|
fileMap := default
|
|
|
|
|
|
|
|
|
|
|
|
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
|
|
|
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
|
|
|
(levelParams : Game.DidOpenLevelParams) (initParams : InitializeParams) :
|
|
|
|
(gameDir : String) (module : Name):
|
|
|
|
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
|
|
|
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
|
|
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
|
|
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
|
|
|
let out ← IO.Process.output
|
|
|
|
let out ← IO.Process.output
|
|
|
|
{ cwd := levelParams.gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
|
|
|
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
|
|
|
if out.exitCode != 0 then
|
|
|
|
if out.exitCode != 0 then
|
|
|
|
throwServerError s!"Error while running Lake: {out.stderr}"
|
|
|
|
throwServerError s!"Error while running Lake: {out.stderr}"
|
|
|
|
|
|
|
|
|
|
|
|
-- Make the paths relative to the current directory
|
|
|
|
-- Make the paths relative to the current directory
|
|
|
|
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
|
|
|
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
|
|
|
let currentDir ← IO.currentDir
|
|
|
|
let currentDir ← IO.currentDir
|
|
|
|
let paths := paths.map fun p => currentDir / (levelParams.gameDir : System.FilePath) / p
|
|
|
|
let paths := paths.map fun p => currentDir / (gameDir : System.FilePath) / p
|
|
|
|
|
|
|
|
|
|
|
|
-- Set the search path
|
|
|
|
-- Set the search path
|
|
|
|
Lean.searchPathRef.set paths
|
|
|
|
Lean.searchPathRef.set paths
|
|
|
|
|
|
|
|
|
|
|
|
let env ← importModules' #[{ module := `Init : Import }, { module := levelParams.levelModule : Import }]
|
|
|
|
let env ← importModules' #[{ module := `Init : Import }, { module := module : Import }]
|
|
|
|
-- return (env, paths)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- use empty header
|
|
|
|
-- use empty header
|
|
|
|
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
|
|
|
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
|
|
|
@ -458,10 +468,11 @@ section Initialization
|
|
|
|
return (headerSnap, srcSearchPath)
|
|
|
|
return (headerSnap, srcSearchPath)
|
|
|
|
|
|
|
|
|
|
|
|
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
|
|
|
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
|
|
|
(levelParams : Game.DidOpenLevelParams) : IO (WorkerContext × WorkerState) := do
|
|
|
|
(gameDir : String) (gameWorkerState : GameWorkerState) : IO (WorkerContext × WorkerState) := do
|
|
|
|
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
|
|
|
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
|
|
|
|
|
|
|
|
|
|
|
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
|
|
|
|
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
|
|
|
|
levelParams initParams
|
|
|
|
gameDir gameWorkerState.levelInfo.module
|
|
|
|
let cancelTk ← CancelToken.new
|
|
|
|
let cancelTk ← CancelToken.new
|
|
|
|
let ctx :=
|
|
|
|
let ctx :=
|
|
|
|
{ hIn := i
|
|
|
|
{ hIn := i
|
|
|
|
@ -472,7 +483,7 @@ section Initialization
|
|
|
|
clientHasWidgets
|
|
|
|
clientHasWidgets
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
|
|
|
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
|
|
|
| Except.ok (s, _) => unfoldSnaps meta #[s] cancelTk levelParams ctx (startAfterMs := 0)
|
|
|
|
| Except.ok (s, _) => unfoldSnaps meta #[s] cancelTk gameWorkerState ctx (startAfterMs := 0)
|
|
|
|
| Except.error e => throw (e : ElabTaskError))
|
|
|
|
| Except.error e => throw (e : ElabTaskError))
|
|
|
|
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
|
|
|
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
|
|
|
return (ctx,
|
|
|
|
return (ctx,
|
|
|
|
@ -509,6 +520,7 @@ section MessageHandling
|
|
|
|
match method with
|
|
|
|
match method with
|
|
|
|
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
|
|
|
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
|
|
|
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
|
|
|
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
|
|
|
|
|
|
|
| "$/setTrace" => pure ()
|
|
|
|
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
|
|
|
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
|
|
|
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
|
|
|
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
|
|
|
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
|
|
|
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
|
|
|
@ -547,26 +559,32 @@ section MainLoop
|
|
|
|
let doc := st.doc
|
|
|
|
let doc := st.doc
|
|
|
|
doc.cancelTk.set
|
|
|
|
doc.cancelTk.set
|
|
|
|
return ()
|
|
|
|
return ()
|
|
|
|
| Message.notification "$/game/setInventory" params =>
|
|
|
|
| Message.request id "shutdown" none =>
|
|
|
|
let p := (← parseParams Game.SetInventoryParams (toJson params))
|
|
|
|
ctx.hOut.writeLspResponse ⟨id, Json.null⟩
|
|
|
|
let s ← get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set {s with levelParams := {s.levelParams with
|
|
|
|
|
|
|
|
inventory := p.inventory,
|
|
|
|
|
|
|
|
difficulty := p.difficulty}}
|
|
|
|
|
|
|
|
mainLoop
|
|
|
|
mainLoop
|
|
|
|
| Message.notification method (some params) =>
|
|
|
|
| Message.notification method (some params) =>
|
|
|
|
handleNotification method (toJson params)
|
|
|
|
handleNotification method (toJson params)
|
|
|
|
mainLoop
|
|
|
|
mainLoop
|
|
|
|
| _ => throwServerError "Got invalid JSON-RPC message"
|
|
|
|
| _ => throwServerError s!"Got invalid JSON-RPC message: {toJson msg}"
|
|
|
|
end MainLoop
|
|
|
|
end MainLoop
|
|
|
|
|
|
|
|
|
|
|
|
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
|
|
|
def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : IO UInt32 := do
|
|
|
|
let i ← maybeTee "fwIn.txt" false i
|
|
|
|
let i ← maybeTee "fwIn.txt" false i
|
|
|
|
let o ← maybeTee "fwOut.txt" true o
|
|
|
|
let o ← maybeTee "fwOut.txt" true o
|
|
|
|
let initParams ← i.readLspRequestAs "initialize" InitializeParams
|
|
|
|
let initRequest ← i.readLspRequestAs "initialize" Game.InitializeParams
|
|
|
|
|
|
|
|
o.writeLspResponse {
|
|
|
|
|
|
|
|
id := initRequest.id
|
|
|
|
|
|
|
|
result := {
|
|
|
|
|
|
|
|
capabilities := Watchdog.mkLeanServerCapabilities
|
|
|
|
|
|
|
|
serverInfo? := some {
|
|
|
|
|
|
|
|
name := "Lean 4 Game Server"
|
|
|
|
|
|
|
|
version? := "0.1.1"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
: InitializeResult
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
discard $ i.readLspNotificationAs "initialized" InitializedParams
|
|
|
|
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
|
|
|
|
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
|
|
|
|
let ⟨_, levelParams⟩ ← i.readLspNotificationAs "$/game/didOpenLevel" Game.DidOpenLevelParams
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let doc := param.textDocument
|
|
|
|
let doc := param.textDocument
|
|
|
|
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
|
|
|
|
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
|
|
|
|
@ -578,9 +596,24 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
|
|
|
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
|
|
|
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
|
|
|
let _ ← IO.setStderr e
|
|
|
|
let _ ← IO.setStderr e
|
|
|
|
try
|
|
|
|
try
|
|
|
|
let (ctx, st) ← initializeWorker meta i o e initParams.param opts levelParams
|
|
|
|
let game ← loadGameData gameDir
|
|
|
|
|
|
|
|
-- TODO: We misuse the `rootUri` field to the gameName
|
|
|
|
|
|
|
|
let rootUri? : Option String := some (toString game.name)
|
|
|
|
|
|
|
|
let initParams := {initRequest.param.toLeanInternal with rootUri?}
|
|
|
|
|
|
|
|
let some (levelId : LevelId) := GameServer.levelIdFromFileName?
|
|
|
|
|
|
|
|
initParams meta.mkInputContext.fileName
|
|
|
|
|
|
|
|
| throwServerError s!"Could not determine level ID: {meta.mkInputContext.fileName}"
|
|
|
|
|
|
|
|
let levelInfo ← loadLevelData gameDir levelId.world levelId.level
|
|
|
|
|
|
|
|
let some initializationOptions := initRequest.param.initializationOptions?
|
|
|
|
|
|
|
|
| throwServerError "no initialization options found"
|
|
|
|
|
|
|
|
let gameWorkerState : GameWorkerState:= {
|
|
|
|
|
|
|
|
inventory := initializationOptions.inventory
|
|
|
|
|
|
|
|
difficulty := initializationOptions.difficulty
|
|
|
|
|
|
|
|
levelInfo
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let (ctx, st) ← initializeWorker meta i o e initParams opts gameDir gameWorkerState
|
|
|
|
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) <|
|
|
|
|
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) <|
|
|
|
|
StateT.run (s := {levelParams := levelParams}) <| (mainLoop)
|
|
|
|
StateT.run (s := gameWorkerState) <| (mainLoop)
|
|
|
|
return (0 : UInt32)
|
|
|
|
return (0 : UInt32)
|
|
|
|
catch e =>
|
|
|
|
catch e =>
|
|
|
|
IO.eprintln e
|
|
|
|
IO.eprintln e
|
|
|
|
@ -590,12 +623,13 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
|
|
|
message := e.toString }] o
|
|
|
|
message := e.toString }] o
|
|
|
|
return (1 : UInt32)
|
|
|
|
return (1 : UInt32)
|
|
|
|
|
|
|
|
|
|
|
|
def workerMain (opts : Options) : IO UInt32 := do
|
|
|
|
def workerMain (opts : Options) (args : List String): IO UInt32 := do
|
|
|
|
let i ← IO.getStdin
|
|
|
|
let i ← IO.getStdin
|
|
|
|
let o ← IO.getStdout
|
|
|
|
let o ← IO.getStdout
|
|
|
|
let e ← IO.getStderr
|
|
|
|
let e ← IO.getStderr
|
|
|
|
try
|
|
|
|
try
|
|
|
|
let exitCode ← initAndRunWorker i o e opts
|
|
|
|
let some gameDir := args[1]? | throwServerError "Expected second argument: gameDir"
|
|
|
|
|
|
|
|
let exitCode ← initAndRunWorker i o e opts gameDir
|
|
|
|
-- HACK: all `Task`s are currently "foreground", i.e. we join on them on main thread exit, but we definitely don't
|
|
|
|
-- HACK: all `Task`s are currently "foreground", i.e. we join on them on main thread exit, but we definitely don't
|
|
|
|
-- want to do that in the case of the worker processes, which can produce non-terminating tasks evaluating user code
|
|
|
|
-- want to do that in the case of the worker processes, which can produce non-terminating tasks evaluating user code
|
|
|
|
o.flush
|
|
|
|
o.flush
|
|
|
|
|