You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lean4game/server/GameServer/FileWorker.lean

801 lines
33 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/- This file is adapted from `Lean/Server/FileWorker.lean`. -/
import Lean.Server.FileWorker
import GameServer.Game
import GameServer.ImportModules
import GameServer.SaveData
import GameServer.EnvExtensions
import GameServer.Tactic.LetIntros
namespace MyModule
open Lean
open Elab
open Parser
private def mkErrorMessage (c : InputContext) (pos : String.Pos) (errorMsg : String) : Message :=
let pos := c.fileMap.toPosition pos
{ fileName := c.fileName, pos := pos, data := errorMsg }
private def mkEOI (pos : String.Pos) : Syntax :=
let atom := mkAtom (SourceInfo.original "".toSubstring pos "".toSubstring pos) ""
mkNode ``Command.eoi #[atom]
partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
(mps : ModuleParserState) (messages : MessageLog) :
Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
let mut pos := mps.pos
let mut recovering := mps.recovering
let mut messages := messages
let mut stx := Syntax.missing -- will always be assigned below
let tokens := getTokenTable pmctx.env
let s := whitespace.run inputCtx pmctx tokens { cache := initCacheForInput inputCtx.input, pos }
let endOfWhitespace := s.pos
let p := (Tactic.sepByIndentSemicolon tacticParser).fn
let s := p.run inputCtx pmctx tokens { cache := initCacheForInput inputCtx.input, pos }
pos := s.pos
match s.errorMsg with
| none =>
stx := s.stxStack.back
recovering := false
| some errorMsg =>
messages := messages.add <| mkErrorMessage inputCtx s.pos (toString errorMsg)
recovering := true
stx := s.stxStack.back
if ¬ inputCtx.input.atEnd s.pos then
messages := messages.add <| mkErrorMessage inputCtx s.pos "end of input"
return (stx, { pos := inputCtx.input.endPos, recovering }, messages, endOfWhitespace)
end MyModule
namespace GameServer.FileWorker
open Lean
open Lean.Server
open Lean.Server.FileWorker
open Lsp
open IO
open Snapshots
open JsonRpc
/--
Game-specific state to be packed on top of the `Server.FileWorker.WorkerState`
used by the Lean server.
-/
structure WorkerState :=
/--
Collection of items which are considered unlocked.
Tactics and theorems are mixed together.
-/
inventory : Array String
/--
Difficulty determines whether tactics/theorems can be locked.
* 0: do not check
* 1: give warnings when locked items are used
* 2: give errors when locked items are used
-/
difficulty : Nat
/--
`levelInfo` contains all the (static) information about the level which is not influenced
by the user's progress.
-/
levelInfo : LevelInfo
deriving ToJson, FromJson
/--
Pack the our custom `WorkerState` on top of the normal worker monad
`Server.FileWorker.WorkerM`.
-/
abbrev WorkerM := StateT WorkerState Server.FileWorker.WorkerM
section Elab
/-- Add a message. use `(severity := .warning)` to specify the severity-/
def addMessage (info : SourceInfo) (inputCtx : Parser.InputContext)
(severity := MessageSeverity.warning) (s : MessageData) :
Elab.Command.CommandElabM Unit := do
modify fun st => { st with
messages := st.messages.add {
fileName := inputCtx.fileName
severity := severity
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
data := s }}
-- TODO: use HashSet for allowed tactics?
/--
Find all tactics in syntax object that are forbidden according to a
set `allowed` of allowed tactics.
-/
partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState : WorkerState)
(stx : Syntax) : Elab.Command.CommandElabM Unit := do
let levelInfo := workerState.levelInfo
-- Parse the syntax object and look for tactics and declarations.
match stx with
| .missing => return ()
| .node _info _kind args =>
-- Go inside a node.
for arg in args do
findForbiddenTactics inputCtx workerState arg
| .atom info val =>
-- Atoms might be tactic names or other keywords.
-- Note: We whitelisted known keywords because we cannot
-- distinguish keywords from tactic names.
let allowed := GameServer.ALLOWED_KEYWORDS
-- Ignore syntax elements that do not start with a letter or are listed above.
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
-- Treat `simp?` and `simp!` like `simp`
let val := val.dropRightWhile (fun c => c == '!' || c == '?')
match levelInfo.tactics.find? (·.name.toString == val) with
| none =>
-- Tactic will never be introduced in the game.
match workerState.inventory.find? (· == val) with
| some _ =>
-- Tactic is in the inventory, allow it.
-- Note: This case shouldn't be possible...
pure ()
| none =>
-- Tactic is not in the inventory.
addMessageByDifficulty info s!"The tactic '{val}' is not available in this game!"
| some tac =>
-- Tactic is introduced at some point in the game.
if tac.disabled then
-- Tactic is disabled in this level.
addMessageByDifficulty info s!"The tactic '{val}' is disabled in this level!"
else if tac.locked then
match workerState.inventory.find? (· == val) with
| none =>
-- Tactic is marked as locked and not in the inventory.
addMessageByDifficulty info s!"You have not unlocked the tactic '{val}' yet!"
| some _ =>
-- Tactic is in the inventory, allow it.
pure ()
| .ident info _rawVal val _preresolved =>
-- Try to resolve the name
let ns ←
try resolveGlobalConst (mkIdent val)
-- Catch "unknown constant" error
catch | _ => pure []
for n in ns do
let some (.thmInfo ..) := (← getEnv).find? n
-- Not a theorem, no checks needed.
| return ()
if some n = levelInfo.statementName then
-- Forbid the theorem we are proving currently
addMessage info inputCtx (severity := .error)
s!"Structural recursion: you can't use '{n}' to proof itself!"
let theoremsAndDefs := levelInfo.lemmas ++ levelInfo.definitions
match theoremsAndDefs.find? (·.name == n) with
| none =>
-- Theorem will never be introduced in this game
addMessageByDifficulty info s!"The theorem/definition '{n}' is not available in this game!"
| some thm =>
-- Theorem is introduced at some point in the game.
if thm.disabled then
-- Theorem is disabled in this level.
addMessageByDifficulty info s!"The theorem/definition '{n}' is disabled in this level!"
else if thm.locked then
match workerState.inventory.find? (· == n.toString) with
| none =>
-- Theorem is still locked.
addMessageByDifficulty info s!"You have not unlocked the theorem/definition '{n}' yet!"
| some _ =>
-- Theorem is in the inventory, allow it.
pure ()
where addMessageByDifficulty (info : SourceInfo) (s : MessageData) :=
-- See `GameServer.FileWorker.WorkerState.difficulty`. Send nothing/warnings/errors
-- depending on difficulty.
let difficulty := workerState.difficulty
if difficulty > 0 then
addMessage info inputCtx (if difficulty > 1 then .error else .warning) s
else pure ()
open Elab Meta Expr in
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
(couldBeEndSnap : Bool) (gameWorkerState : WorkerState)
(initParams : Lsp.InitializeParams) : IO Snapshot := do
-- Recognize end snap
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
let endSnap : Snapshot := {
beginPos := snap.mpState.pos
stx := MyModule.mkEOI snap.mpState.pos
mpState := snap.mpState
cmdState := snap.cmdState
interactiveDiags := ← withNewInteractiveDiags snap.msgLog
tacticCache := snap.tacticCache
}
return endSnap
let parseResultRef ← IO.mkRef (Syntax.missing, snap.mpState)
let cmdStateRef ← IO.mkRef snap.cmdState
/- The same snapshot may be executed by different tasks. So, to make sure `elabCommandTopLevel` has exclusive
access to the cache, we create a fresh reference here. Before this change, the
following `snap.tacticCache.modify` would reset the tactic post cache while another snapshot was still using it. -/
let tacticCacheNew ← IO.mkRef (← snap.tacticCache.get)
let cmdCtx : Elab.Command.Context := {
cmdPos := snap.endPos
fileName := inputCtx.fileName
fileMap := inputCtx.fileMap
tacticCache? := some tacticCacheNew
}
let (output, _) ← IO.FS.withIsolatedStreams (isolateStderr := server.stderrAsMessages.get snap.cmdState.scopes.head!.opts) <| liftM (m := BaseIO) do
Elab.Command.catchExceptions
(getResetInfoTrees *> do
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
| panic! s!"Level not found: {inputCtx.fileName} / {GameServer.levelIdFromFileName? initParams inputCtx.fileName}"
let scope := level.scope
-- use open namespaces and options as in the level file
Elab.Command.withScope (fun _ => scope) do
for od in scope.openDecls do
let .simple ns _ := od
| pure ()
activateScoped ns
activateScoped scope.currNamespace
-- parse tactics
let pmctx := {
env := ← getEnv,
options := scope.opts,
currNamespace := scope.currNamespace,
openDecls := scope.openDecls }
let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
MyModule.parseTactic inputCtx pmctx snap.mpState snap.msgLog
modify (fun s => { s with messages := msgLog })
parseResultRef.set (tacticStx, cmdParserState)
-- Check for forbidden tactics
findForbiddenTactics inputCtx gameWorkerState tacticStx
-- 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 #[]
-- Insert final `done` command to display unsolved goal error in the end
let done := Syntax.node (.synthetic cmdParserState.pos cmdParserState.pos) ``Lean.Parser.Tactic.done #[]
let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩)
let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*)
-- Always call `let_intros` to get rid `let` statements in the goal.
-- This makes the experience for the user much nicer and allows for local
-- definitions in the exercise.
let cmdStx ← `(command|
theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx⟩)} )
Elab.Command.elabCommandTopLevel cmdStx)
cmdCtx cmdStateRef
let postNew := (← tacticCacheNew.get).post
snap.tacticCache.modify fun _ => { pre := postNew, post := {} }
let mut postCmdState ← cmdStateRef.get
if !output.isEmpty then
postCmdState := {
postCmdState with
messages := postCmdState.messages.add {
fileName := inputCtx.fileName
severity := MessageSeverity.information
pos := inputCtx.fileMap.toPosition snap.endPos
data := output
}
}
let (tacticStx, cmdParserState) ← parseResultRef.get
if tacticStx.isMissing then throwServerError "Tactic execution went wrong. No stx found."
let postCmdSnap : Snapshot := {
beginPos := tacticStx.getPos?.getD 0
stx := tacticStx
mpState := cmdParserState
cmdState := postCmdState
interactiveDiags := ← withNewInteractiveDiags postCmdState.messages
tacticCache := (← IO.mkRef {})
}
return postCmdSnap
where
/-- Compute the current interactive diagnostics log by finding a "diff" relative to the parent
snapshot. We need to do this because unlike the `MessageLog` itself, interactive diags are not
part of the command state. -/
withNewInteractiveDiags (msgLog : MessageLog) : IO (PersistentArray Widget.InteractiveDiagnostic) := do
let newMsgCount := msgLog.msgs.size - snap.msgLog.msgs.size
let mut ret := snap.interactiveDiags
for i in List.iota newMsgCount do
let newMsg := msgLog.msgs.get! (msgLog.msgs.size - i)
ret := ret.push (← Widget.msgToInteractiveDiagnostic inputCtx.fileMap newMsg hasWidgets)
return ret
private def publishIleanInfo (method : String) (m : DocumentMeta) (hOut : FS.Stream)
(snaps : Array Snapshot) : IO Unit := do
let trees := snaps.map fun snap => snap.infoTree
let references ← findModuleRefs m.text trees (localVars := true) |>.toLspModuleRefs
let param := { version := m.version, references : LeanIleanInfoParams }
hOut.writeLspNotification { method, param }
private def publishIleanInfoUpdate : DocumentMeta → FS.Stream → Array Snapshot → IO Unit :=
publishIleanInfo "$/lean/ileanInfoUpdate"
private def publishIleanInfoFinal : DocumentMeta → FS.Stream → Array Snapshot → IO Unit :=
publishIleanInfo "$/lean/ileanInfoFinal"
structure GameCompletedParams where
uri : String
deriving ToJson, FromJson
structure GameDiagnostics where
diagnostics : List Diagnostic
deriving ToJson, FromJson
structure GameParams where
uri : String
diagnostics : GameDiagnostics
deriving ToJson, FromJson
-- `snap` and `initParams` are unused
set_option linter.unusedVariables false in
/-- WIP: publish diagnostics, all intermediate goals and if the game is completed. -/
def publishProofState (m : DocumentMeta) (snap : Snapshot) (initParams : Lsp.InitializeParams) (hOut : FS.Stream) :
IO Unit := do
-- let text := m.text
-- -- `snap` is the one snapshot containing the entire proof.
-- let mut goals : Array <| InteractiveGoalsWithHints := #[]
-- for pos in text.positions do
-- let source := text.getLineBefore pos
-- -- iterate over all newlines in the proof and get the goals and hints at each position
-- if let goalsAtResult@(_ :: _) := snap.infoTree.goalsAt? text pos then
-- pure ()
-- let goalAtPos : List <| List InteractiveGoalWithHints ← goalsAtResult.mapM
-- fun { ctxInfo := ci, tacticInfo := tacticInfo, useAfter := useAfter, .. } => do
-- -- TODO: What does this function body do?
-- -- let ciAfter := { ci with mctx := ti.mctxAfter }
-- let ci := if useAfter then
-- { ci with mctx := tacticInfo.mctxAfter }
-- else
-- { ci with mctx := tacticInfo.mctxBefore }
-- -- compute the interactive goals
-- let goalMvars : List MVarId ← ci.runMetaM {} do
-- return if useAfter then tacticInfo.goalsAfter else tacticInfo.goalsBefore
-- let interactiveGoals : List InteractiveGoalWithHints ← ci.runMetaM {} do
-- goalMvars.mapM fun goal => do
-- let hints ← findHints goal m initParams
-- let interactiveGoal ← goalToInteractive goal
-- return ⟨interactiveGoal, hints⟩
-- -- TODO: This code is way old, can it be deleted?
-- -- compute the goal diff
-- -- let goals ← ciAfter.runMetaM {} (do
-- -- try
-- -- Widget.diffInteractiveGoals useAfter ti goals
-- -- catch _ =>
-- -- -- fail silently, since this is just a bonus feature
-- -- return goals
-- -- )
-- return interactiveGoals
-- let goalAtPos : Array InteractiveGoalWithHints := ⟨goalAtPos.foldl (· ++ ·) []⟩
-- goals := goals.push ⟨goalAtPos, source⟩
-- else
-- -- No goals present
-- goals := goals.push default
-- -- Question: Is there a difference between the diags of this snap and the last snap?
-- -- Should we get the diags from there?
-- let diag : Array Widget.InteractiveDiagnostic := snap.interactiveDiags.toArray
-- -- Level is completed if there are no errors or warnings
-- let completed : Bool := ¬ diag.any (fun d =>
-- d.severity? == some .error d.severity? == some .warning)
-- let param : ProofState := {
-- steps := goals,
-- diagnostics := diag,
-- completed := completed }
-- TODO
let param := { uri := m.uri : GameCompletedParams}
hOut.writeLspNotification { method := "$/game/publishProofState", param }
/-- Checks whether game level has been completed and sends a notification to the client -/
def publishGameCompleted (m : DocumentMeta) (hOut : FS.Stream) (snaps : Array Snapshot) : IO Unit := do
-- check if there is any error or warning
for snap in snaps do
if snap.diagnostics.any fun d => d.severity? == some .error d.severity? == some .warning
then return
let param := { uri := m.uri : GameCompletedParams}
hOut.writeLspNotification { method := "$/game/completed", param }
/-- copied from `Lean.Server.FileWorker.nextCmdSnap`. -/
-- @[inherit_doc Lean.Server.FileWorker.nextCmdSnap] -- cannot inherit from private
private def nextCmdSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
(gameWorkerState : WorkerState) (initParams : Lsp.InitializeParams) :
AsyncElabM (Option Snapshot) := do
cancelTk.check
let s ← get
let .some lastSnap := s.snaps.back? | panic! "empty snapshots"
if lastSnap.isAtEnd then
publishDiagnostics m lastSnap.diagnostics.toArray ctx.hOut
publishProgressDone m ctx.hOut
publishIleanInfoFinal m ctx.hOut s.snaps
return none
publishProgressAtPos m lastSnap.endPos ctx.hOut
-- (modified part)
-- Make sure that there is at least one snap after the head snap, so that
-- we can see the current goal even on an empty document
let couldBeEndSnap := s.snaps.size > 1
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
gameWorkerState initParams
set { s with snaps := s.snaps.push snap }
cancelTk.check
-- publishProofState m snap initParams ctx.hOut
publishDiagnostics m snap.diagnostics.toArray ctx.hOut
publishIleanInfoUpdate m ctx.hOut #[snap]
return some snap
-- Copied from `Lean.Server.FileWorker.unfoldCmdSnaps` using our own `nextCmdSnap`.
@[inherit_doc Lean.Server.FileWorker.unfoldCmdSnaps]
def unfoldCmdSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
(startAfterMs : UInt32) (gameWorkerState : WorkerState)
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
let ctx ← read
let some headerSnap := snaps[0]? | panic! "empty snapshots"
if headerSnap.msgLog.hasErrors then
publishProgressAtPos m headerSnap.beginPos ctx.hOut (kind := LeanFileProgressKind.fatalError)
publishIleanInfoFinal m ctx.hOut #[headerSnap]
return AsyncList.ofList [headerSnap]
else
publishIleanInfoUpdate m ctx.hOut snaps
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
IO.sleep startAfterMs
AsyncList.unfoldAsync (nextCmdSnap ctx m cancelTk gameWorkerState ctx.initParams) { snaps })
end Elab
section Updates
/-- Given the new document, updates editable doc state. -/
def updateDocument (newMeta : DocumentMeta) : WorkerM Unit := do
let s ← get
let ctx ← read
let oldDoc := (← StateT.lift get).doc
oldDoc.cancelTk.set
let initHeaderStx := (← StateT.lift get).initHeaderStx
let (newHeaderStx, newMpState, _) ← Parser.parseHeader newMeta.mkInputContext
let cancelTk ← CancelToken.new
let headSnapTask := oldDoc.cmdSnaps.waitHead?
let newSnaps ← if initHeaderStx != newHeaderStx then
EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
IO.sleep ctx.initParams.editDelay.toUInt32
cancelTk.check
IO.Process.exit 2
else EIO.mapTask (ε := ElabTaskError) (t := headSnapTask) (prio := .dedicated) fun headSnap?? => do
-- There is always at least one snapshot absent exceptions
let some headSnap ← MonadExcept.ofExcept headSnap?? | panic! "empty snapshots"
let newHeaderSnap := { headSnap with stx := newHeaderStx, mpState := newMpState }
let changePos := oldDoc.meta.text.source.firstDiffPos newMeta.text.source
-- Ignore exceptions, we are only interested in the successful snapshots
let (cmdSnaps, _) ← oldDoc.cmdSnaps.getFinishedPrefix
-- NOTE(WN): we invalidate eagerly as `endPos` consumes input greedily. To re-elaborate only
-- when really necessary, we could do a whitespace-aware `Syntax` comparison instead.
let mut validSnaps ← pure (cmdSnaps.takeWhile (fun s => s.endPos < changePos))
if h : validSnaps.length ≤ 1 then
validSnaps := [newHeaderSnap]
else
/- When at least one valid non-header snap exists, it may happen that a change does not fall
within the syntactic range of that last snap but still modifies it by appending tokens.
We check for this here. We do not currently handle crazy grammars in which an appended
token can merge two or more previous commands into one. To do so would require reparsing
the entire file. -/
have : validSnaps.length ≥ 2 := Nat.gt_of_not_le h
let mut lastSnap := validSnaps.getLast (by subst ·; simp at h)
let preLastSnap :=
have : 0 < validSnaps.length := Nat.lt_of_lt_of_le (by decide) this
have : validSnaps.length - 2 < validSnaps.length := Nat.sub_lt this (by decide)
validSnaps[validSnaps.length - 2]
let newLastStx ← parseNextCmd newMeta.mkInputContext preLastSnap
if newLastStx != lastSnap.stx then
validSnaps := validSnaps.dropLast
-- 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)
unfoldCmdSnaps newMeta validSnaps.toArray cancelTk s ctx
(startAfterMs := ctx.initParams.editDelay.toUInt32)
StateT.lift <| modify fun st => { st with
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
end Updates
section Initialization
def DocumentMeta.mkInputContext (doc : DocumentMeta) : Parser.InputContext where
input := "" -- No header!
fileName := (System.Uri.fileUriToPath? doc.uri).getD doc.uri |>.toString
fileMap := default
/-- `gameDir` and `module` were added.
TODO: In general this resembles little similarity with the
original code, and I don't know why...
-/
-- @[inherit_doc Lean.Server.FileWorker.compileHeader]
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
(gameDir : String) (module : Name):
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
let out ← IO.Process.output
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
if out.exitCode != 0 then
throwServerError s!"Error while running Lake: {out.stderr}"
-- Make the paths relative to the current directory
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
let currentDir ← IO.currentDir
let paths := paths.map fun p => currentDir / (gameDir : System.FilePath) / p
-- Set the search path
Lean.searchPathRef.set paths
let env ← importModules' #[{ module := `Init : Import }, { module := module : Import }]
-- use empty header
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
{m.mkInputContext with
input := ""
fileMap := FileMap.ofString ""}
(headerStx, ·) <$> EIO.asTask do
let mut srcSearchPath : SearchPath := paths --← initSrcSearchPath (← getBuildDir)
let headerEnv := env
let mut headerEnv := headerEnv
try
if let some path := System.Uri.fileUriToPath? m.uri then
headerEnv := headerEnv.setMainModule (← moduleNameOfFileName path none)
catch _ => pure ()
let cmdState := Elab.Command.mkState headerEnv {} opts
let cmdState := { cmdState with infoState := {
enabled := true
trees := #[Elab.InfoTree.context (.commandCtx {
env := headerEnv
fileMap := m.text
ngen := { namePrefix := `_worker }
}) (Elab.InfoTree.node
(Elab.Info.ofCommandInfo { elaborator := `header, stx := headerStx })
(headerStx[1].getArgs.toList.map (fun importStx =>
Elab.InfoTree.node (Elab.Info.ofCommandInfo {
elaborator := `import
stx := importStx
}) #[].toPArray'
)).toPArray'
)].toPArray'
}}
let headerSnap := {
beginPos := 0
stx := headerStx
mpState := {} -- was `headerParserState`
cmdState := cmdState
interactiveDiags := ← cmdState.messages.msgs.mapM (Widget.msgToInteractiveDiagnostic m.text · hasWidgets)
tacticCache := (← IO.mkRef {})
}
publishDiagnostics m headerSnap.diagnostics.toArray hOut
return (headerSnap, srcSearchPath)
/-- Copied from `Lean.Server.FileWorker.initializeWorker`. Added `gameDir` and
`gameWorkerState` arguments and use custom `unfoldCmdSnaps`. -/
-- @[inherit_doc Lean.Server.FileWorker.initializeWorker]
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
(gameDir : String) (gameWorkerState : WorkerState) : IO (WorkerContext × Server.FileWorker.WorkerState) := do
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
(gameDir := gameDir) (module := gameWorkerState.levelInfo.module)
let cancelTk ← CancelToken.new
let ctx := {
hIn := i
hOut := o
hLog := e
headerTask
initParams
clientHasWidgets
}
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
| Except.ok (s, _) => unfoldCmdSnaps meta #[s] cancelTk gameWorkerState ctx (startAfterMs := 0)
| Except.error e => throw (e : ElabTaskError))
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
return (ctx, {
doc := doc
initHeaderStx := headerStx
currHeaderStx := headerStx
importCachingTask? := none
pendingRequests := RBMap.empty
rpcSessions := RBMap.empty
})
end Initialization
section NotificationHandling
/-- Copied from `Lean.Server.FileWorker.handleDidChange` but with our custom `WorkerM` and
`updateDocument` -/
-- @[inherit_doc Lean.Server.FileWorker.handleDidChange]
def handleDidChange (p : DidChangeTextDocumentParams) : WorkerM Unit := do
let docId := p.textDocument
let changes := p.contentChanges
let oldDoc := (← StateT.lift get).doc -- needed a lift to our custom `WorkerM`
let newVersion := docId.version?.getD 0
if ¬ changes.isEmpty then
let newDocText := foldDocumentChanges changes oldDoc.meta.text
-- modification: set the `DependencyBuildMode` from
-- `oldDoc.meta.dependencyBuildMode` to `.always`
updateDocument ⟨docId.uri, newVersion, newDocText, .always⟩
end NotificationHandling
section MessageHandling
/--
Modified notification handler.
Compare to `Lean.Server.FileWorker.handleNotification`.
We use the modified `WorkerM` and use our custom `handleDidChange`.
-/
def handleNotification (method : String) (params : Json) : WorkerM Unit := do
let handle := fun paramType [FromJson paramType] (handler : paramType → WorkerM Unit) =>
(StateT.lift <| parseParams paramType params) >>= handler
match method with
-- Modified `textDocument/didChange`, using a custom `handleDidChange`
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
-- unmodified
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
-- unmodified
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
-- unmodified
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
-- New. TODO: What is this for?
| "$/setTrace" => pure ()
| _ => throwServerError s!"Got unsupported notification method: {method}"
end MessageHandling
section MainLoop
/--
The main-loop. Copied from `Lean.Server.FileWorker.mainLoop`. Use custom `WorkerM` as well
as custom `handleNotification`.
-/
--@[inherit_doc Lean.Server.FileWorker.mainLoop]
partial def mainLoop : WorkerM Unit := do
let ctx ← read
let mut st ← StateT.lift get
let msg ← ctx.hIn.readLspMessage
-- Erase finished tasks if there are no errors.
let filterFinishedTasks (acc : PendingRequestMap) (id : RequestID) (task : Task (Except IO.Error Unit))
: IO PendingRequestMap := do
if (← hasFinished task) then
if let Except.error e := task.get then
throwServerError s!"Failed responding to request {id}: {e}"
pure <| acc.erase id
else pure acc
let pendingRequests ← st.pendingRequests.foldM (fun acc id task => filterFinishedTasks acc id task) st.pendingRequests
st := { st with pendingRequests }
for (id, seshRef) in st.rpcSessions do
let sesh ← seshRef.get
if (← sesh.hasExpired) then
st := { st with rpcSessions := st.rpcSessions.erase id }
set st
-- Process the RPC-message and restart main-loop.
match msg with
| Message.request id "shutdown" none =>
--added. TODO: why do we need that? Or has it just removed in Lean since when we started?
ctx.hOut.writeLspResponse ⟨id, Json.null⟩
mainLoop
| Message.request id method (some params) =>
-- Requests are handled by the unmodified lean server.
handleRequest id method (toJson params)
mainLoop
| Message.notification "exit" none =>
let doc := st.doc
doc.cancelTk.set
doc.cmdSnaps.cancel
return ()
| Message.notification method (some params) =>
-- Custom notification handler
handleNotification method (toJson params)
mainLoop
| _ =>
throwServerError s!"Got invalid JSON-RPC message: {toJson msg}"
end MainLoop
/-- Modified from `Lean.Server.FileWorker.initAndRunWorker`.
Added `gameDir` argument, -/
-- @[inherit_doc Lean.Server.FileWorker.initAndRunWorker]
def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : IO UInt32 := do
let i ← maybeTee "fwIn.txt" false i
let o ← maybeTee "fwOut.txt" true o
-- BIG MODIFICATION
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 doc := param.textDocument
-- modification: using `.always`
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap, .always⟩
let e := e.withPrefix s!"[{param.textDocument.uri}] "
let _ ← IO.setStderr e
try
-- BIG MODIFICATION
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 : WorkerState := {
inventory := initializationOptions.inventory
difficulty := initializationOptions.difficulty
levelInfo
}
let (ctx, st) ← initializeWorker meta i o e initParams opts gameDir gameWorkerState
-- Run the main loop
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) <|
StateT.run (s := gameWorkerState) <| (mainLoop)
return (0 : UInt32)
catch e =>
IO.eprintln e
publishDiagnostics meta #[{
range := ⟨⟨0, 0⟩, ⟨0, 0⟩⟩,
severity? := DiagnosticSeverity.error,
message := e.toString }] o
return (1 : UInt32)
/--
The main function. Simply wrapping `initAndRunWorker`.
Copied from `Lean.Server.FileWorker.workerMain`. We add `args` as an argument to pass on
the `gameDir`.
TODO: The first arg `args[0]` is always expected to be `--server`. We could drop this completely.
-/
-- @[inherit_doc Lean.Server.FileWorker.workerMain]
def workerMain (opts : Options) (args : List String): IO UInt32 := do
let i ← IO.getStdin
let o ← IO.getStdout
let e ← IO.getStderr
try
let some gameDir := args[1]? | throwServerError "Expected second argument: gameDir"
let exitCode ← initAndRunWorker i o e opts gameDir
o.flush
e.flush
IO.Process.exit exitCode.toUInt8
catch err =>
e.putStrLn s!"worker initialization error: {err}"
return (1 : UInt32)
end GameServer.FileWorker