/- This is the Lean 4 game server. It offers a way to interact with Lean 4 which is completely distinct from running the command-line lean or the language server protocol. It is based on lean-gym by Daniel Selsam. -/ import Lean.Data.Json.Basic import GameServer.Utils import GameServer.EnvExtensions import GameServer.Communication open Lean Meta Elab Tactic Std /-- Convert format to string without line breaks -/ def Std.Format.oneline : Format → String | .nil => "" | .line => "" | .text s => s | .nest _ f => f.oneline | .append f g => f.oneline ++ g.oneline | .group f _ => f.oneline | .tag _ f => f.oneline /-- Convert JSON to string without line breaks -/ instance instToStringJsonOneLine : ToString Json := ⟨fun o => o.render.oneline⟩ attribute [-instance] Lean.Json.instToStringJson /-! ## GameGoal -/ structure GameGoal where objects : List LocalDecl assumptions : List LocalDecl goal : String mvarid : MVarId def Lean.MVarId.toGameGoal (goal : MVarId) : MetaM GameGoal := do match (← getMCtx).findDecl? goal with | none => throwError "unknown goal" | some mvarDecl => do -- toGameGoalAux below will sort local declarations from the context of goal into data and assumptions, -- discarding auxilliary declarations let rec toGameGoalAux : List (Option LocalDecl) → MetaM (List LocalDecl × List LocalDecl) | (some decl)::t => withLCtx mvarDecl.lctx mvarDecl.localInstances do let (o, a) ← toGameGoalAux t if decl.isAuxDecl then return (o, a) if (← inferType decl.type).isProp then return (o, decl::a) else return (decl::o, a) | none:: t => toGameGoalAux t | [] => return ([], []) withLCtx mvarDecl.lctx mvarDecl.localInstances do let (objects, assumptions) ← toGameGoalAux mvarDecl.lctx.decls.toList return {objects := objects, assumptions := assumptions, goal := toString (← Meta.ppExpr mvarDecl.type), mvarid := goal } def GameGoal.toJson (gameGoal : GameGoal) : MetaM Json := gameGoal.mvarid.withContext do return Json.mkObj [("objects", Lean.ToJson.toJson (← gameGoal.objects.mapM Lean.LocalDecl.toJson)), ("assumptions", Lean.ToJson.toJson (← gameGoal.assumptions.mapM Lean.LocalDecl.toJson)), ("goal", gameGoal.goal)] /-! ## Action -/ inductive Action where | info : Action | loadLevel : Nat → Action | runTactic : String → Action | undo : Action | restartGame : Action | restartLevel : Action | quit : Action | next : Action | prev : Action | invalid : String → Action -- Used for broken Json parsing deriving ToJson, FromJson, Repr def Action.parse (s : String) : Action := let e : Except String Action := try return ← fromJson? (← Json.parse s) catch _ => return Action.invalid s match e with | Except.ok a => a | _ => Action.info def Action.get : IO Action := return Action.parse (← (← IO.getStdin).getLine) /-! ## LevelInfo -/ structure LevelInfo extends GameLevel where goals : List GameGoal := [] errors : Array String := #[] message : String := "" def LevelInfo.toJson (info : LevelInfo) : MetaM Json := return Json.mkObj [("index", Lean.ToJson.toJson info.index), ("title", Lean.ToJson.toJson info.title), ("tactics", Lean.ToJson.toJson info.tactics), ("lemmas", Lean.ToJson.toJson info.lemmas), ("errors", Lean.ToJson.toJson info.errors), ("goals", Lean.ToJson.toJson (← info.goals.mapM (·.toJson))), ("message", info.message)] namespace Server /-! ## LevelM and Response -/ structure SavedState where tacticState : Tactic.SavedState message : String := "" abbrev LevelM := ReaderT GameLevel StateRefT (Array SavedState) TermElabM /-- Returns the most recent SavedState. -/ def getState : LevelM SavedState := do let some state := (← get).back? | throwError "Couldn't find tactic state" return state structure Response : Type where goals : List GameGoal := [] errors : Array String := #[] message : String := "" def Response.toJson (resp : Response) : MetaM Json := return Json.mkObj [("errors", Lean.ToJson.toJson resp.errors), ("goals", Lean.ToJson.toJson (← resp.goals.mapM (·.toJson))), ("message", resp.message)] /-! ## Main running functions -/ /-- Dummy `Core.Context` value to be fed to `Lean.Core.CoreM.toIO` -/ def coreCtx : Core.Context := { currNamespace := Name.anonymous, openDecls := [], fileName := "", fileMap := { source := "", positions := #[0], lines := #[1] } } structure RunTacticParams where tactic : String deriving FromJson structure LoadLevelParams where number : Nat deriving FromJson partial def runLevel (requestId : JsonRpc.RequestID) (env : Environment) (GameName : Name) (levels : HashMap Nat GameLevel) (idx : Nat) : IO Unit := do let levelName : Name := s!"Level{toString idx}" let termElabM : TermElabM Unit := do let some lvl := levels.find? idx | throwError s!"Cannot find level {idx}" let mvar ← mkFreshExprMVar (some lvl.goal) (kind := MetavarKind.synthetic) let (_, mvar) ← mvar.mvarId!.introNP lvl.intro_nb mvar.withContext do let state := #[{tacticState := { term := ← Term.saveState, tactic := { goals := [mvar] }}}] let levelM : LevelM Unit := do let resp := {← mkResponse with message := lvl.introduction} let levelInfo : LevelInfo := { index := lvl.index, title := lvl.title, tactics := lvl.tactics, lemmas := lvl.lemmas, errors := resp.errors, goals := resp.goals, message := resp.message } let hOut ← IO.getStdout hOut.writeGspResponse ⟨requestId, ← levelInfo.toJson⟩ mainLoop levelM.run lvl |>.run' state let metaM : MetaM Unit := termElabM.run' (ctx := {}) try discard <| metaM.run'.toIO coreCtx { env := env } catch | .userError s => output s!"Could not run level {idx}: {s}" | _ => output s!"Could not run level {idx}" where mkResponse (errors : Array String := #[]) : LevelM Response := do let savedState ← getState let goals := savedState.tacticState.tactic.goals return { goals := (← liftM $ goals.mapM Lean.MVarId.toGameGoal), message := savedState.message, errors := errors } /-- Try to parse the given String as a single line tactic invocation. Update the LevelM state only if the tactic succeeds. Always return a Response object which contains information about the current state with a message and errors if any. -/ runTactic (tacticString : String) : LevelM Response := do let lvl ← read let tacticNames := (lvl.tactics.map (·.name.toString)).toList let tacName := (tacticString.trim.splitOn " ").headD "" if not (tacticNames.contains tacName) then mkResponse #["Unrecognized tactic"] else let savedState ← getState match Parser.runParserCategory (← getEnv) `tactic tacticString "" with | Except.error err => mkResponse #[err] | Except.ok stx => do savedState.tacticState.term.restore let mvarId : MVarId := savedState.tacticState.tactic.goals.head! try let unsolvedGoals ← Tactic.run mvarId do set savedState.tacticState.tactic evalTactic stx if (← getThe Core.State).messages.hasErrors then let messages := (← getThe Core.State).messages.getErrorMessages.toList.toArray mkResponse (← (messages.map Message.data).mapM fun md => md.toString) else let savedState : Tactic.SavedState := { term := (← Term.saveState), tactic := { goals := unsolvedGoals}} let mut message := "" match unsolvedGoals with | goal::_ => do let normalized_tgt ← normalizedRevertExpr goal for msg in lvl.messages do if ← isDefEq normalized_tgt msg.normalized_goal then message := msg.message break | [] => pure () if unsolvedGoals matches [] then message := (← read).conclusion modify fun s => s.push {tacticState := savedState, message := message} mkResponse catch ex => mkResponse #[← ex.toMessageData.toString] mainLoop : LevelM Unit := do let hIn ← IO.getStdin let hOut ← IO.getStdout let hLog ← IO.getStderr let m ← hIn.readGspMessage match m with | .request id "runTactic" params => do match fromJson? (toJson (params)) with | Except.ok (v : RunTacticParams) => let resp ← runTactic v.tactic hOut.writeGspResponse ⟨id, ← resp.toJson⟩ | Except.error inner => hLog.putStr s!"Invalid params: {inner}" hLog.flush | .request id "loadLevel" (some params) => match fromJson? (toJson (params)) with | Except.ok (v : LoadLevelParams) => runLevel id env GameName levels v.number | Except.error inner => hLog.putStr s!"Invalid params: {inner}" hLog.flush | .request id "undo" _ => do modify fun s => s.pop hOut.writeGspResponse ⟨id, ← (← mkResponse).toJson⟩ | _ => hLog.putStr s!"Invalid action: {toJson m}" hLog.flush mainLoop open System Lean Std in partial def runGame (GameName : Name) : IO Unit := do let env ← importModules [{ module := `Init : Import }, { module := GameName : Import }] {} 0 let termElabM : TermElabM Unit := do let levels := levelsExt.getState env let game := {← gameExt.get with nb_levels := levels.size } mainLoop env game levels let metaM : MetaM Unit := termElabM.run' (ctx := {}) discard <| metaM.run'.toIO coreCtx { env := env } where mainLoop (env : Environment) (game : Game) (levels : HashMap Nat GameLevel): IO Unit := do let hIn ← IO.getStdin let hOut ← IO.getStdout let hLog ← IO.getStderr let m ← hIn.readGspMessage match m with | .request id "info" params => hOut.writeGspResponse ⟨id, game⟩ | .request id "loadLevel" (some params) => match fromJson? (toJson (params)) with | Except.ok (v : LoadLevelParams) => runLevel id env GameName levels v.number | Except.error inner => hLog.putStr s!"Invalid params: {inner}" hLog.flush | .request id m _ => hOut.writeGspResponseError ⟨id, JsonRpc.ErrorCode.methodNotFound, s!"Method not found: {m}", none⟩ | _ => hLog.putStr s!"Invalid action: {toJson m}" hLog.flush mainLoop env game levels end Server