Compare commits
24
Commits
v4.6.0-bump
...
v4.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f84a3426 | ||
|
|
217f86ce5e | ||
|
|
dd60093dfc | ||
|
|
85347a54d9 | ||
|
|
3b4afd6e0e | ||
|
|
2c12872a6e | ||
|
|
ad819bf7ff | ||
|
|
c0f366abba | ||
|
|
f72ebdf050 | ||
|
|
af8463ca5d | ||
|
|
d0a444205a | ||
|
|
1796c76a84 | ||
|
|
a75a4a81ac | ||
|
|
45d84103c1 | ||
|
|
92e9ed38b2 | ||
|
|
d689c7ec86 | ||
|
|
16c979a6c2 | ||
|
|
2b85386373 | ||
|
|
8008b68fd6 | ||
|
|
2649f985fa | ||
|
|
698a88c545 | ||
|
|
3775ad98c8 | ||
|
|
780514e45a | ||
|
|
19f2ceface |
@@ -1,6 +1,8 @@
|
||||
name: Build
|
||||
run-name: Build the project
|
||||
on: [push]
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -5,15 +5,33 @@ import { DeletedChatContext, ProofContext } from "./infoview/context";
|
||||
import { lastStepHasErrors } from "./infoview/goals";
|
||||
import { Button } from "./button";
|
||||
|
||||
/** Plug-in the variable names in a hint. We do this client-side to prepare
|
||||
* for i18n in the future. i.e. one should be able translate the `rawText`
|
||||
* and have the variables substituted just before displaying.
|
||||
*/
|
||||
function getHintText(hint: GameHint): string {
|
||||
if (hint.rawText) {
|
||||
// Replace the variable names used in the hint with the ones used by the player
|
||||
// variable names are marked like `«{g}»` inside the text.
|
||||
return hint.rawText.replaceAll(/«\{(.*?)\}»/g, ((_, v) =>
|
||||
// `hint.varNames` contains tuples `[oldName, newName]`
|
||||
(hint.varNames.find(x => x[0] == v))[1]))
|
||||
} else {
|
||||
// hints created in the frontend do not have a `rawText`
|
||||
// TODO: `hint.text` could be removed in theory.
|
||||
return hint.text
|
||||
}
|
||||
}
|
||||
|
||||
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
||||
<Markdown>{hint.text}</Markdown>
|
||||
<Markdown>{getHintText(hint)}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function HiddenHint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||
return <div className={`message warning step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
||||
<Markdown>{hint.text}</Markdown>
|
||||
<Markdown>{getHintText(hint)}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -31,7 +49,7 @@ export function Hints({hints, showHidden, step, selected, toggleSelection, lastL
|
||||
|
||||
export function DeletedHint({hint} : {hint: GameHint}) {
|
||||
return <div className="message information deleted-hint">
|
||||
<Markdown>{hint.text}</Markdown>
|
||||
<Markdown>{getHintText(hint)}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ interface GoalsProps {
|
||||
|
||||
export function Goals({ goals, filter }: GoalsProps) {
|
||||
if (goals.goals.length === 0) {
|
||||
return <>No goals</>
|
||||
return <></>
|
||||
} else {
|
||||
return <>
|
||||
{goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g.goal} filter={filter} />)}
|
||||
|
||||
@@ -232,7 +232,11 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
||||
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
|
||||
} else {
|
||||
ret = <div className="infoview vscode-light">
|
||||
{proof.completed && <div className="level-completed">Level completed! 🎉</div>}
|
||||
{proof.completedWithWarnings &&
|
||||
<div className="level-completed">
|
||||
{proof.completed ? "Level completed! 🎉" : "Level completed with warnings 🎭"}
|
||||
</div>
|
||||
}
|
||||
<Infos />
|
||||
<Hints hints={proof.steps[curPos?.line]?.goals[0]?.hints}
|
||||
showHidden={showHelp.has(curPos?.line)} step={curPos?.line}
|
||||
|
||||
@@ -49,6 +49,8 @@ export interface InteractiveTermGoal extends InteractiveGoalCore {
|
||||
export interface GameHint {
|
||||
text: string;
|
||||
hidden: boolean;
|
||||
rawText: string;
|
||||
varNames: string[][]; // in Lean: `Array (Name × Name)`
|
||||
}
|
||||
|
||||
export interface InteractiveGoalWithHints {
|
||||
|
||||
@@ -41,6 +41,13 @@
|
||||
.level-completed {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
padding-top: .2em;
|
||||
padding-bottom: .2em;
|
||||
border-radius: .5em;
|
||||
background-color: #eee;
|
||||
|
||||
}
|
||||
|
||||
.typewriter {
|
||||
|
||||
@@ -370,6 +370,6 @@ td code {
|
||||
}
|
||||
|
||||
/* DEBUG */
|
||||
.proof .step {
|
||||
/* .proof .step {
|
||||
border: 2px solid rgb(0, 123, 255);
|
||||
}
|
||||
} */
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
Here are some issues experienced by users.
|
||||
|
||||
- You can reset the lake projects involved (i.e. the `server/` folder here as well as your [game's folder](https://github.com/hhu-adam/GameSkeleton)) with the following commands:
|
||||
```
|
||||
cd [THE PROJECT]
|
||||
rm -rf .lake/
|
||||
lake update -R
|
||||
lake build
|
||||
```
|
||||
If you experience problems related to Lean or lake, you should first try to reset it this way.
|
||||
|
||||
# VSCode Dev-Container
|
||||
* If you don't get the pop-up, you might have disabled them, and you can reenable it by
|
||||
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
||||
|
||||
Generated
+17
-6
@@ -8753,9 +8753,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
|
||||
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz",
|
||||
"integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ=="
|
||||
},
|
||||
"node_modules/ip-anonymize": {
|
||||
"version": "0.1.0",
|
||||
@@ -14341,6 +14341,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/stacktrace-parser/node_modules/type-fest": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
|
||||
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -14902,9 +14910,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
|
||||
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
|
||||
"version": "4.10.3",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.3.tgz",
|
||||
"integrity": "sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import GameServer.Helpers
|
||||
import GameServer.Inventory
|
||||
import GameServer.Options
|
||||
import GameServer.SaveData
|
||||
import GameServer.Hints
|
||||
import I18n
|
||||
|
||||
open Lean Meta Elab Command
|
||||
|
||||
@@ -32,16 +34,17 @@ elab "Level" level:num : command => do
|
||||
|
||||
/-- Define the title of the current game/world/level. -/
|
||||
elab "Title" t:str : command => do
|
||||
let title ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
|
||||
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
|
||||
| .Level => modifyCurLevel fun level => pure {level with title := title}
|
||||
| .World => modifyCurWorld fun world => pure {world with title := title}
|
||||
| .Game => modifyCurGame fun game => pure {game with
|
||||
title := t.getString
|
||||
tile := {game.tile with title := t.getString}}
|
||||
tile := {game.tile with title := title}}
|
||||
|
||||
/-- Define the introduction of the current game/world/level. -/
|
||||
elab "Introduction" t:str : command => do
|
||||
let intro := t.getString
|
||||
let intro ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with introduction := intro}
|
||||
| .World => modifyCurWorld fun world => pure {world with introduction := intro}
|
||||
@@ -49,7 +52,7 @@ elab "Introduction" t:str : command => do
|
||||
|
||||
/-- Define the info of the current game. Used for e.g. credits -/
|
||||
elab "Info" t:str : command => do
|
||||
let info:= t.getString
|
||||
let info ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level =>
|
||||
logError "Can't use `Info` in a level!"
|
||||
@@ -81,7 +84,7 @@ elab "Image" t:str : command => do
|
||||
/-- Define the conclusion of the current game or current level if some
|
||||
building a level. -/
|
||||
elab "Conclusion" t:str : command => do
|
||||
let conclusion := t.getString
|
||||
let conclusion ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with conclusion := conclusion}
|
||||
| .World => modifyCurWorld fun world => pure {world with conclusion := conclusion}
|
||||
@@ -94,13 +97,13 @@ elab "Prerequisites" t:str* : command => do
|
||||
|
||||
/-- Short caption for the game (1 sentence) -/
|
||||
elab "CaptionShort" t:str : command => do
|
||||
let caption := t.getString
|
||||
let caption ← t.getString.translate
|
||||
modifyCurGame fun game => pure {game with
|
||||
tile := {game.tile with short := caption}}
|
||||
|
||||
/-- More detailed description what the game is about (2-4 sentences). -/
|
||||
elab "CaptionLong" t:str : command => do
|
||||
let caption := t.getString
|
||||
let caption ← t.getString.translate
|
||||
modifyCurGame fun game => pure {game with
|
||||
tile := {game.tile with long := caption}}
|
||||
|
||||
@@ -141,6 +144,7 @@ TacticDoc rw "`rw` stands for rewrite, etc. "
|
||||
-/
|
||||
elab doc:docComment ? "TacticDoc" name:ident content:str ? : command => do
|
||||
let doc ← parseDocCommentLegacy doc content
|
||||
let doc ← doc.translate
|
||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||
type := .Tactic
|
||||
name := name.getId
|
||||
@@ -165,6 +169,7 @@ The theorem/definition to have the same fully qualified name as in mathlib.
|
||||
elab doc:docComment ? "TheoremDoc" name:ident "as" displayName:str "in" category:str content:str ? :
|
||||
command => do
|
||||
let doc ← parseDocCommentLegacy doc content
|
||||
let doc ← doc.translate
|
||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||
type := .Lemma
|
||||
name := name.getId
|
||||
@@ -194,6 +199,7 @@ The theorem/definition to have the same fully qualified name as in mathlib.
|
||||
-/
|
||||
elab doc:docComment ? "DefinitionDoc" name:ident "as" displayName:str template:str ? : command => do
|
||||
let doc ← parseDocCommentLegacy doc template
|
||||
let doc ← doc.translate
|
||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||
type := .Definition
|
||||
name := name.getId,
|
||||
@@ -340,6 +346,9 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
let lvlIdx ← getCurLevelIdx
|
||||
|
||||
let docContent ← parseDocComment doc
|
||||
let docContent ← match docContent with
|
||||
| none => pure none
|
||||
| some d => d.translate
|
||||
|
||||
-- Save the messages before evaluation of the proof.
|
||||
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
|
||||
@@ -396,12 +405,41 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
.nest hidden $
|
||||
.compose (.ofGoal text) (.ofGoal goal) := msg.data then
|
||||
let hint ← liftTermElabM $ withMCtx ctx.mctx $ withLCtx ctx.lctx #[] $ withEnv ctx.env do
|
||||
|
||||
let goalDecl ← goal.getDecl
|
||||
let fvars := goalDecl.lctx.decls.toArray.filterMap id |> Array.map (·.fvarId)
|
||||
|
||||
-- NOTE: This code about `hintFVarsNames` is duplicated from `RpcHandlers`
|
||||
-- where the variable bijection is constructed, and they
|
||||
-- need to be matching.
|
||||
-- NOTE: This is a bit a hack of somebody who does not know how meta-programming works.
|
||||
-- All we want here is a list of `userNames` for the `FVarId`s in `hintFVars`...
|
||||
-- and we wrap them in `«{}»` here since I don't know how to do it later.
|
||||
let mut hintFVarsNames : Array Expr := #[]
|
||||
for fvar in fvars do
|
||||
let name₁ ← fvar.getUserName
|
||||
hintFVarsNames := hintFVarsNames.push <| Expr.fvar ⟨s!"«\{{name₁}}»"⟩
|
||||
|
||||
let text ← instantiateMVars (mkMVar text)
|
||||
|
||||
-- Evaluate the text in the `Hint`'s context to get the old variable names.
|
||||
let rawText := (← GameServer.evalHintMessage text) hintFVarsNames
|
||||
let ctx₂ := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}}
|
||||
let rawText : String ← (MessageData.withContext ctx₂ rawText).toString
|
||||
|
||||
return {
|
||||
goal := ← abstractCtx goal
|
||||
text := ← instantiateMVars (mkMVar text)
|
||||
text := text
|
||||
rawText := rawText
|
||||
strict := strict == 1
|
||||
hidden := hidden == 1
|
||||
}
|
||||
|
||||
-- Note: The current setup for hints is a bit convoluted, but for now we need to
|
||||
-- send the text once through i18n to register it in the env extension.
|
||||
-- This could probably be rewritten once i18n works fully.
|
||||
let _ ← hint.rawText.translate
|
||||
|
||||
hints := hints.push hint
|
||||
else
|
||||
nonHintMsgs := nonHintMsgs.push msg
|
||||
@@ -440,6 +478,8 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
|
||||
/-! # Hints -/
|
||||
|
||||
open GameServer in
|
||||
|
||||
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
|
||||
see hints. The tactic does not affect the goal state.
|
||||
-/
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import GameServer.AbstractCtx
|
||||
import GameServer.Graph
|
||||
import GameServer.Hints
|
||||
|
||||
open GameServer
|
||||
|
||||
-- TODO: Is there a better place?
|
||||
/-- Keywords that the server should not consider as tactics. -/
|
||||
def GameServer.ALLOWED_KEYWORDS : List String :=
|
||||
["with", "fun", "at", "only", "by", "generalizing"]
|
||||
|
||||
/-- The default game name if `Game "MyGame"` is not used. -/
|
||||
def defaultGameName: String := "MyGame"
|
||||
@@ -18,22 +24,6 @@ defined in this file.
|
||||
|
||||
open Lean
|
||||
|
||||
/-! ## Hints -/
|
||||
|
||||
/-- A hint to help the user with a specific goal state -/
|
||||
structure GoalHintEntry where
|
||||
goal : AbstractCtxResult
|
||||
/-- Text of the hint as an expression of type `Array Expr → MessageData` -/
|
||||
text : Expr
|
||||
/-- If true, then hint should be hidden and only be shown on player's request -/
|
||||
hidden : Bool := false
|
||||
/-- If true, then the goal must contain only the assumptions specified in `goal` and no others -/
|
||||
strict : Bool := false
|
||||
|
||||
instance : Repr GoalHintEntry := {
|
||||
reprPrec := fun a n => reprPrec a.text n
|
||||
}
|
||||
|
||||
/-! ## Inventory (documentation)
|
||||
|
||||
The inventory contains documentation that the user can access.
|
||||
|
||||
@@ -122,7 +122,7 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState :
|
||||
-- Atoms might be tactic names or other keywords.
|
||||
-- Note: We whitelisted known keywords because we cannot
|
||||
-- distinguish keywords from tactic names.
|
||||
let allowed := ["with", "fun", "at", "only", "by", "to", "generalizing", "says"]
|
||||
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`
|
||||
@@ -169,15 +169,20 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState :
|
||||
match theoremsAndDefs.find? (·.name == n) with
|
||||
| none =>
|
||||
-- Theorem will never be introduced in this game
|
||||
addMessageByDifficulty info s!"You have not unlocked the theorem/definition '{n}' yet!"
|
||||
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
|
||||
-- Theorem is still locked.
|
||||
addMessageByDifficulty info s!"You have not unlocked the theorem/definition '{n}' yet!"
|
||||
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
|
||||
@@ -299,7 +304,7 @@ where
|
||||
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)
|
||||
let references ← findModuleRefs m.text trees (localVars := true) |>.toLspModuleRefs
|
||||
let param := { version := m.version, references : LeanIleanInfoParams }
|
||||
hOut.writeLspNotification { method, param }
|
||||
|
||||
@@ -386,60 +391,60 @@ def publishProofState (m : DocumentMeta) (snap : Snapshot) (initParams : Lsp.Ini
|
||||
|
||||
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 }
|
||||
/-- 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
|
||||
/-- 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
|
||||
-- (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
|
||||
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 })
|
||||
-- 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
|
||||
|
||||
@@ -503,6 +508,12 @@ def DocumentMeta.mkInputContext (doc : DocumentMeta) : Parser.InputContext where
|
||||
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
|
||||
@@ -538,7 +549,7 @@ def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWid
|
||||
let cmdState := Elab.Command.mkState headerEnv {} opts
|
||||
let cmdState := { cmdState with infoState := {
|
||||
enabled := true
|
||||
trees := #[Elab.InfoTree.context ({
|
||||
trees := #[Elab.InfoTree.context (.commandCtx {
|
||||
env := headerEnv
|
||||
fileMap := m.text
|
||||
ngen := { namePrefix := `_worker }
|
||||
@@ -555,7 +566,7 @@ def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWid
|
||||
let headerSnap := {
|
||||
beginPos := 0
|
||||
stx := headerStx
|
||||
mpState := {}
|
||||
mpState := {} -- was `headerParserState`
|
||||
cmdState := cmdState
|
||||
interactiveDiags := ← cmdState.messages.msgs.mapM (Widget.msgToInteractiveDiagnostic m.text · hasWidgets)
|
||||
tacticCache := (← IO.mkRef {})
|
||||
|
||||
@@ -4,8 +4,6 @@ import Lean
|
||||
|
||||
open Lean Meta Elab Command
|
||||
|
||||
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
||||
|
||||
/-! ## Doc Comment Parsing -/
|
||||
|
||||
/-- Read a doc comment and get its content. Return `""` if no doc comment available. -/
|
||||
@@ -85,23 +83,6 @@ def getStatementString (name : Name) : CommandElabM String := do
|
||||
syntax statementAttr := "(" &"attr" ":=" Parser.Term.attrInstance,* ")"
|
||||
-- TODO
|
||||
|
||||
|
||||
/-- Remove any spaces at the beginning of a new line -/
|
||||
partial def removeIndentation (s : String) : String :=
|
||||
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
||||
let c := s.get i
|
||||
let i := s.next i
|
||||
if s.atEnd i then
|
||||
acc.push c
|
||||
else if removeSpaces && c == ' ' then
|
||||
loop i acc (removeSpaces := true)
|
||||
else if c == '\n' then
|
||||
loop i (acc.push c) (removeSpaces := true)
|
||||
else
|
||||
loop i (acc.push c)
|
||||
loop ⟨0⟩ ""
|
||||
|
||||
|
||||
/-! ## Loops in Graph-like construct
|
||||
|
||||
TODO: Why are we not using graphs here but our own construct `HashMap Name (HashSet Name)`?
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import GameServer.AbstractCtx
|
||||
|
||||
/-!
|
||||
This file contains anything related to the `Hint` tactic used to add hints to a game level.
|
||||
-/
|
||||
|
||||
open Lean Meta Elab
|
||||
|
||||
namespace GameServer
|
||||
|
||||
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
||||
|
||||
/-- A hint to help the user with a specific goal state -/
|
||||
structure GoalHintEntry where
|
||||
goal : AbstractCtxResult
|
||||
/-- Text of the hint as an expression of type `Array Expr → MessageData` -/
|
||||
text : Expr
|
||||
rawText : String
|
||||
/-- If true, then hint should be hidden and only be shown on player's request -/
|
||||
hidden : Bool := false
|
||||
/-- If true, then the goal must contain only the assumptions specified in `goal` and no others -/
|
||||
strict : Bool := false
|
||||
|
||||
instance : Repr GoalHintEntry := {
|
||||
reprPrec := fun a n => reprPrec a.text n
|
||||
}
|
||||
|
||||
/-- For a hint `(hint : GoalHintEntry)` one uses `(← evalHintMessage hint.text) x`
|
||||
where `(x : Array Expr)` contains the names of all the variables that should be inserted
|
||||
in the text.
|
||||
|
||||
TODO: explain better. -/
|
||||
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
||||
evalExpr (Array Expr → MessageData)
|
||||
(.forallE default (mkApp (mkConst ``Array [levelZero]) (mkConst ``Expr))
|
||||
(mkConst ``MessageData) .default)
|
||||
|
||||
@[implemented_by evalHintMessageUnsafe]
|
||||
def evalHintMessage : Expr → MetaM (Array Expr → MessageData) := fun _ => pure (fun _ => "")
|
||||
|
||||
/-- Remove any spaces at the beginning of a new line -/
|
||||
partial def removeIndentation (s : String) : String :=
|
||||
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
||||
let c := s.get i
|
||||
let i := s.next i
|
||||
if s.atEnd i then
|
||||
acc.push c
|
||||
else if removeSpaces && c == ' ' then
|
||||
loop i acc (removeSpaces := true)
|
||||
else if c == '\n' then
|
||||
loop i (acc.push c) (removeSpaces := true)
|
||||
else
|
||||
loop i (acc.push c)
|
||||
loop ⟨0⟩ ""
|
||||
@@ -146,7 +146,7 @@ def goalToInteractive (mvarId : MVarId) : MetaM InteractiveGoal := do
|
||||
return {
|
||||
hyps
|
||||
type := goalFmt
|
||||
ctx := ⟨← Elab.ContextInfo.save⟩
|
||||
ctx := ⟨{← Elab.CommandContextInfo.save with }⟩
|
||||
userName?
|
||||
goalPrefix := getGoalPrefix mvarDecl
|
||||
mvarId
|
||||
|
||||
@@ -121,7 +121,7 @@ partial def collectUsedInventory (stx : Syntax) (acc : UsedInventory := {}) : Co
|
||||
| .atom _info val =>
|
||||
-- ignore syntax elements that do not start with a letter
|
||||
-- and ignore some standard keywords
|
||||
let allowed := ["with", "fun", "at", "only", "by"]
|
||||
let allowed := GameServer.ALLOWED_KEYWORDS
|
||||
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`
|
||||
return {acc with tactics := acc.tactics.insert val}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import GameServer.EnvExtensions
|
||||
import GameServer.InteractiveGoal
|
||||
import Std.Data.Array.Init.Basic
|
||||
import GameServer.Hints
|
||||
|
||||
open Lean
|
||||
open Server
|
||||
@@ -103,14 +104,6 @@ def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (in
|
||||
then return some bij
|
||||
else return none
|
||||
|
||||
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
||||
evalExpr (Array Expr → MessageData)
|
||||
(.forallE default (mkApp (mkConst ``Array [levelZero]) (mkConst ``Expr))
|
||||
(mkConst ``MessageData) .default)
|
||||
|
||||
@[implemented_by evalHintMessageUnsafe]
|
||||
def evalHintMessage : Expr → MetaM (Array Expr → MessageData) := fun _ => pure (fun _ => "")
|
||||
|
||||
open Meta in
|
||||
/-- Find all hints whose trigger matches the current goal -/
|
||||
def findHints (goal : MVarId) (m : DocumentMeta) (initParams : Lsp.InitializeParams) : MetaM (Array GameHint) := do
|
||||
@@ -121,14 +114,42 @@ def findHints (goal : MVarId) (m : DocumentMeta) (initParams : Lsp.InitializePar
|
||||
openAbstractCtxResult hint.goal fun hintFVars hintGoal => do
|
||||
if let some fvarBij := matchExpr (← instantiateMVars $ hintGoal) (← instantiateMVars $ ← inferType $ mkMVar goal)
|
||||
then
|
||||
let lctx := (← goal.getDecl).lctx
|
||||
if let some bij ← matchDecls hintFVars lctx.getFVars (strict := hint.strict) (initBij := fvarBij)
|
||||
|
||||
-- NOTE: This code for `hintFVarsNames` is also duplicated in the
|
||||
-- "Statement" command, where `hint.rawText` is created. They need to be matching.
|
||||
-- NOTE: This is a bit a hack of somebody who does not know how meta-programming works.
|
||||
-- All we want here is a list of `userNames` for the `FVarId`s in `hintFVars`...
|
||||
-- and we wrap them in `«{}»` here since I don't know how to do it later.
|
||||
let mut hintFVarsNames : Array Expr := #[]
|
||||
for fvar in hintFVars do
|
||||
let name₁ ← fvar.fvarId!.getUserName
|
||||
hintFVarsNames := hintFVarsNames.push <| Expr.fvar ⟨s!"«\{{name₁}}»"⟩
|
||||
|
||||
let lctx := (← goal.getDecl).lctx -- the player's local context
|
||||
if let some bij ← matchDecls hintFVars lctx.getFVars
|
||||
(strict := hint.strict) (initBij := fvarBij)
|
||||
then
|
||||
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
|
||||
-- Evaluate the text in the player's context to get the new variable names.
|
||||
let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar)
|
||||
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := lctx, opts := {}}
|
||||
let text ← (MessageData.withContext ctx text).toString
|
||||
return some { text := text, hidden := hint.hidden }
|
||||
|
||||
-- Here we map the goal's variable names to the player's variable names.
|
||||
let mut varNames : Array <| Name × Name := #[]
|
||||
for (fvar₁, fvar₂) in bij.forward.toArray do
|
||||
-- get the `userName` of the fvar in the opened local context of the hint.
|
||||
let name₁ ← fvar₁.getUserName
|
||||
-- get the `userName` in the player's local context.
|
||||
let name₂ := (lctx.get! fvar₂).userName
|
||||
varNames := varNames.push (name₁, name₂)
|
||||
|
||||
return some {
|
||||
text := text,
|
||||
hidden := hint.hidden,
|
||||
rawText := hint.rawText,
|
||||
varNames := varNames }
|
||||
|
||||
else return none
|
||||
else
|
||||
return none
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import GameServer.EnvExtensions
|
||||
import I18n
|
||||
|
||||
open Lean Meta Elab Command
|
||||
|
||||
|
||||
/-! ## Copy images -/
|
||||
|
||||
open IO.FS System FilePath in
|
||||
@@ -59,6 +59,9 @@ def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name))
|
||||
|
||||
IO.FS.writeFile (path / inventoryFileName) (toString (toJson inventory))
|
||||
|
||||
-- write PO file for translation
|
||||
I18n.createPOTemplate
|
||||
|
||||
open GameData
|
||||
|
||||
def loadData (f : System.FilePath) (α : Type) [FromJson α] : IO α := do
|
||||
|
||||
@@ -54,8 +54,17 @@ deriving RpcEncodable
|
||||
|
||||
/-- A hint in the game at the corresponding goal. -/
|
||||
structure GameHint where
|
||||
/-- The text with the variable names already inserted.
|
||||
|
||||
Note: This is in theory superfluous and will be completely replaced by `rawText`. We just left
|
||||
it in for debugging for now. -/
|
||||
text : String
|
||||
/-- Flag whether the hint should be hidden initially. -/
|
||||
hidden : Bool
|
||||
/-- The text with the variables not inserted yet. -/
|
||||
rawText : String
|
||||
/-- The assignment of variable names in the `rawText` to the ones the player used. -/
|
||||
varNames : Array <| Name × Name
|
||||
deriving FromJson, ToJson
|
||||
|
||||
/-- Bundled `InteractiveGoal` together with an array of hints that apply at this stage. -/
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
[{"url": "https://github.com/leanprover/std4.git",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"rev": "08ec2584b1892869e3a5f4122b029989bcb4ca79",
|
||||
"rev": "a7543d1a6934d52086971f510e482d743fe30cf3",
|
||||
"name": "std",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "v4.5.0",
|
||||
"inputRev": "v4.6.0",
|
||||
"inherited": false,
|
||||
"configFile": "lakefile.lean"},
|
||||
{"url": "https://github.com/hhu-adam/lean-i18n.git",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"rev": "c5b84feffb28dbd5b1ac74b3bf63271296fabfa5",
|
||||
"name": "i18n",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "v4.6.0",
|
||||
"inherited": false,
|
||||
"configFile": "lakefile.lean"}],
|
||||
"name": "GameServer",
|
||||
|
||||
@@ -7,8 +7,7 @@ package GameServer
|
||||
def leanVersion : String := s!"v{Lean.versionString}"
|
||||
|
||||
require std from git "https://github.com/leanprover/std4.git" @ leanVersion
|
||||
|
||||
-- require importGraph from git "https://github.com/leanprover-community/import-graph" @ leanVersion
|
||||
require i18n from git "https://github.com/hhu-adam/lean-i18n.git" @ leanVersion
|
||||
|
||||
lean_lib GameServer
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
leanprover/lean4:v4.5.0
|
||||
leanprover/lean4:v4.6.0
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": [
|
||||
"ES2021.String"
|
||||
]
|
||||
},
|
||||
"exclude": ["server", "relay"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user