Merge branch 'main' of github.com:leanprover-community/lean4game

pull/43/head
Jon Eugster 2 years ago
commit c065dcc5e3

@ -22,9 +22,9 @@ import './level.css'
import { Button } from './Button' import { Button } from './Button'
import { ConnectionContext, useLeanClient } from '../connection'; import { ConnectionContext, useLeanClient } from '../connection';
import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api'; import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api';
import { codeEdited, selectCode } from '../state/progress'; import { codeEdited, selectCode, progressSlice } from '../state/progress';
import { useAppDispatch } from '../hooks'; import { useAppDispatch, useAppSelector } from '../hooks';
import { useSelector } from 'react-redux'; import { useStore } from 'react-redux';
import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts'; import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts';
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'; import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection';
@ -63,7 +63,7 @@ function Level() {
const codeviewRef = useRef<HTMLDivElement>(null) const codeviewRef = useRef<HTMLDivElement>(null)
const introductionPanelRef = useRef<HTMLDivElement>(null) const introductionPanelRef = useRef<HTMLDivElement>(null)
const initialCode = useSelector(selectCode(worldId, levelId)) const initialCode = useAppSelector(selectCode(worldId, levelId))
const [commandLineMode, setCommandLineMode] = useState(true) const [commandLineMode, setCommandLineMode] = useState(true)
const [commandLineInput, setCommandLineInput] = useState("") const [commandLineInput, setCommandLineInput] = useState("")
@ -114,6 +114,8 @@ function Level() {
const gameInfo = useGetGameInfoQuery() const gameInfo = useGetGameInfoQuery()
useLoadWorldFiles(worldId)
const level = useLoadLevelQuery({world: worldId, level: levelId}) const level = useLoadLevelQuery({world: worldId, level: levelId})
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -277,8 +279,8 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
let model = monaco.editor.getModel(uri) let model = monaco.editor.getModel(uri)
if (!model) { if (!model) {
model = monaco.editor.createModel(initialCode, 'lean4', uri) model = monaco.editor.createModel(initialCode, 'lean4', uri)
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
} }
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
editor.setModel(model) editor.setModel(model)
editor.setPosition(model.getFullModelRange().getEndPosition()) editor.setPosition(model.getFullModelRange().getEndPosition())
@ -288,9 +290,32 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
const taskGutter = new LeanTaskGutter(infoProvider.client, editor) const taskGutter = new LeanTaskGutter(infoProvider.client, editor)
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor) const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
return () => { abbrevRewriter.dispose(); taskGutter.dispose(); model.dispose() } return () => { abbrevRewriter.dispose(); taskGutter.dispose(); }
} }
}, [editor, levelId, connection, leanClientStarted]) }, [editor, levelId, connection, leanClientStarted])
return {editor, infoProvider, editorConnection} return {editor, infoProvider, editorConnection}
} }
/** Open all files in this world on the server so that they will load faster when accessed */
function useLoadWorldFiles(worldId) {
const gameInfo = useGetGameInfoQuery()
const store = useStore()
useEffect(() => {
if (gameInfo.data) {
const models = []
for (let levelId = 1; levelId <= gameInfo.data.worldSize[worldId]; levelId++) {
const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`)
let model = monaco.editor.getModel(uri)
if (model) {
models.push(model)
} else {
const code = selectCode(worldId, levelId)(store.getState())
models.push(monaco.editor.createModel(code, 'lean4', uri))
}
}
return () => { for (let model of models) { model.dispose() } }
}
}, [gameInfo.data, worldId])
}

6
package-lock.json generated

@ -21,7 +21,7 @@
"cytoscape-klay": "^3.1.4", "cytoscape-klay": "^3.1.4",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"express": "^4.18.2", "express": "^4.18.2",
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?master", "lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
"lean4web": "github:hhu-adam/lean4web", "lean4web": "github:hhu-adam/lean4web",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"react": "^18.2.0", "react": "^18.2.0",
@ -6048,8 +6048,8 @@
"node_modules/lean4-infoview": { "node_modules/lean4-infoview": {
"name": "@leanprover/infoview", "name": "@leanprover/infoview",
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?master", "resolved": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
"integrity": "sha512-XthMPhgRGoP/3lClX5LEQNXQYmzEcIQQBuK6AZ4RC5NKcTU7XUpA0qtZTQcWkPeZbOqz02FX5Dg8f4P1CDMuGw==", "integrity": "sha512-xrKjl6/+Myn0UeBmEgkba6LStBfjb4iIC7fSJ3tAJxG0CrsKBmBCeU/sfjah3RNw7ieetLeFGd6YvxtoI4nPhQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@leanprover/infoview-api": "~0.2.1", "@leanprover/infoview-api": "~0.2.1",

@ -17,7 +17,7 @@
"cytoscape-klay": "^3.1.4", "cytoscape-klay": "^3.1.4",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"express": "^4.18.2", "express": "^4.18.2",
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?master", "lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
"lean4web": "github:hhu-adam/lean4web", "lean4web": "github:hhu-adam/lean4web",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"react": "^18.2.0", "react": "^18.2.0",

@ -1,7 +1,7 @@
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import express from 'express' import express from 'express'
import path from 'path' import path from 'path'
import { spawn } from 'child_process'; import * as cp from 'child_process';
import * as url from 'url'; import * as url from 'url';
import * as rpc from 'vscode-ws-jsonrpc'; import * as rpc from 'vscode-ws-jsonrpc';
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server'; import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
@ -34,7 +34,34 @@ if (isDevelopment) {
cwd = "." cwd = "."
} }
/** We keep a queue of started Lean Server processes to be ready when a user arrives */
const queue = []
const queueLength = 5
/** start Lean Server processes to refill the queue */
function fillQueue() {
while (queue.length < queueLength) {
const serverProcess = cp.spawn(cmd, cmdArgs, { cwd })
serverProcess.on('error', error =>
console.error(`Launching Lean Server failed: ${error}`)
);
if (serverProcess.stderr !== null) {
serverProcess.stderr.on('data', data =>
console.error(`Lean Server: ${data}`)
);
}
queue.push(serverProcess)
}
}
fillQueue()
wss.addListener("connection", function(ws) { wss.addListener("connection", function(ws) {
const ps = queue.shift() // Pick the first Lean process; it's likely to be ready immediately
fillQueue()
const socket = { const socket = {
onMessage: (cb) => { ws.on("message", cb) }, onMessage: (cb) => { ws.on("message", cb) },
onError: (cb) => { ws.on("error", cb) }, onError: (cb) => { ws.on("error", cb) },
@ -44,7 +71,7 @@ wss.addListener("connection", function(ws) {
const reader = new rpc.WebSocketMessageReader(socket); const reader = new rpc.WebSocketMessageReader(socket);
const writer = new rpc.WebSocketMessageWriter(socket); const writer = new rpc.WebSocketMessageWriter(socket);
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close()) const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
const serverConnection = jsonrpcserver.createServerProcess('Lean Server', cmd, cmdArgs, { cwd }); const serverConnection = jsonrpcserver.createProcessStreamConnection(ps);
socketConnection.forward(serverConnection, message => { socketConnection.forward(serverConnection, message => {
console.log(`CLIENT: ${JSON.stringify(message)}`) console.log(`CLIENT: ${JSON.stringify(message)}`)
return message; return message;

@ -79,9 +79,12 @@ elab "Statement" statementName:ident ? descr:str sig:declSig val:declVal : comma
-- | none => `(lemma $(mkIdent "XX") $sig $val) -- TODO: Make it into an `example` -- | none => `(lemma $(mkIdent "XX") $sig $val) -- TODO: Make it into an `example`
-- | some name => `(lemma $name $sig $val) -- | some name => `(lemma $name $sig $val)
let scope ← getScope
elabCommand thmStatement elabCommand thmStatement
modifyCurLevel fun level => pure {level with modifyCurLevel fun level => pure {level with
goal := sig, goal := sig,
scope := scope,
descrText := descr.getString, descrText := descr.getString,
descrFormat := match statementName with descrFormat := match statementName with
| none => "example " ++ (toString <| reprint sig.raw) ++ " := by" | none => "example " ++ (toString <| reprint sig.raw) ++ " := by"
@ -187,17 +190,28 @@ elab "TacticDoc" name:ident content:str : command =>
instance : Quote TacticDocEntry `term := instance : Quote TacticDocEntry `term :=
⟨λ entry => Syntax.mkCApp ``TacticDocEntry.mk #[quote entry.name, quote entry.content]⟩ ⟨λ entry => Syntax.mkCApp ``TacticDocEntry.mk #[quote entry.name, quote entry.content]⟩
/-- Throw an error if tactic doc does not exist -/
def checkTacticDoc (name : Name) : CommandElabM Unit := do
let some _ := (tacticDocExt.getState (← getEnv)).find? (·.name = name)
| throwError "Missing Tactic Documentation: {name}"
/-- Declare tactics that are introduced by this level. -/ /-- Declare tactics that are introduced by this level. -/
elab "NewTactics" args:ident* : command => do elab "NewTactics" args:ident* : command => do
modifyCurLevel fun level => pure {level with newTactics := args.map (·.getId)} let names := args.map (·.getId)
for name in names do checkTacticDoc name
modifyCurLevel fun level => pure {level with newTactics := names}
/-- Declare tactics that are temporarily disabled in this level -/ /-- Declare tactics that are temporarily disabled in this level -/
elab "DisabledTactics" args:ident* : command => do elab "DisabledTactics" args:ident* : command => do
modifyCurLevel fun level => pure {level with disabledTactics := args.map (·.getId)} let names := args.map (·.getId)
for name in names do checkTacticDoc name
modifyCurLevel fun level => pure {level with disabledTactics := names}
/-- Temporarily disable all tactics except the ones declared here -/ /-- Temporarily disable all tactics except the ones declared here -/
elab "OnlyTactics" args:ident* : command => do elab "OnlyTactics" args:ident* : command => do
modifyCurLevel fun level => pure {level with onlyTactics := args.map (·.getId)} let names := args.map (·.getId)
for name in names do checkTacticDoc name
modifyCurLevel fun level => pure {level with onlyTactics := names}
/-! ## Lemmas -/ /-! ## Lemmas -/

@ -161,9 +161,10 @@ deriving ToJson, FromJson, Repr
def getCurLevelId [MonadError m] : m LevelId := do def getCurLevelId [MonadError m] : m LevelId := do
return { game := ← getCurGameId, world := ← getCurWorldId, level := ← getCurLevelIdx} return { game := ← getCurGameId, world := ← getCurWorldId, level := ← getCurLevelIdx}
/-- /-- Instance to make GameLevel Repr work -/
instance : Repr Elab.Command.Scope := ⟨fun s _ => repr s.currNamespace⟩
/--
Fields: Fields:
- TODO - TODO
- introduction: Theory block shown all the time. - introduction: Theory block shown all the time.
@ -195,6 +196,7 @@ structure GameLevel where
lemmas: Array Availability := default lemmas: Array Availability := default
hints: Array GoalHintEntry := default hints: Array GoalHintEntry := default
goal : TSyntax `Lean.Parser.Command.declSig := default goal : TSyntax `Lean.Parser.Command.declSig := default
scope : Elab.Command.Scope := default
descrText: String := default descrText: String := default
descrFormat : String := default descrFormat : String := default
deriving Inhabited, Repr deriving Inhabited, Repr

@ -77,13 +77,24 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (level : GameL
| .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
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ val != "with" then if 0 < val.length ∧ val.data[0]!.isAlpha ∧ val != "with" ∧ val != "at" then
if ¬ ((level.tactics.map (·.name.toString))).contains val then if ¬ ((level.tactics.map (·.name.toString))).contains val then
addErrorMessage info s!"You have not unlocked the tactic '{val}' yet!" addErrorMessage info s!"You have not unlocked the tactic '{val}' yet!"
else if level.disabledTactics.contains val else if level.disabledTactics.contains val
(¬ level.onlyTactics.isEmpty ∧ ¬ level.onlyTactics.contains val)then (¬ level.onlyTactics.isEmpty ∧ ¬ level.onlyTactics.contains val)then
addErrorMessage info s!"The tactic '{val}' is disabled in this level!" addErrorMessage info s!"The tactic '{val}' is disabled in this level!"
| .ident info rawVal val preresolved => return () | .ident info rawVal val preresolved =>
let ns ←
try resolveGlobalConst (mkIdent val)
catch | _ => pure [] -- catch "unknown constant" error
for n in ns do
let some (.thmInfo ..) := (← getEnv).find? n
| return () -- not a theroem -> ignore
if ¬ ((level.lemmas.map (·.name))).contains n then
addErrorMessage info s!"You have not unlocked the lemma '{n}' yet!"
else if level.disabledLemmas.contains n
(¬ level.onlyLemmas.isEmpty ∧ ¬ level.onlyLemmas.contains n)then
addErrorMessage info s!"The lemma '{n}' is disabled in this level!"
where addErrorMessage (info : SourceInfo) (s : MessageData) := where addErrorMessage (info : SourceInfo) (s : MessageData) :=
modify fun st => { st with modify fun st => { st with
messages := st.messages.add { messages := st.messages.add {
@ -139,10 +150,10 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩) let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩)
let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*) let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*)
let cmdStx ← `(command| let cmdStx ← `(command| theorem the_theorem $(level.goal) := by {$(⟨tacticStx⟩)} )
set_option tactic.hygienic false in
theorem the_theorem $(level.goal) := by {$(⟨tacticStx⟩)} ) Elab.Command.withScope (fun _ => level.scope) do -- use open namespaces and options as in the file
Elab.Command.elabCommandTopLevel cmdStx) Elab.Command.elabCommandTopLevel cmdStx)
cmdCtx cmdStateRef cmdCtx cmdStateRef
let postNew := (← tacticCacheNew.get).post let postNew := (← tacticCacheNew.get).post
snap.tacticCache.modify fun _ => { pre := postNew, post := {} } snap.tacticCache.modify fun _ => { pre := postNew, post := {} }

@ -102,6 +102,7 @@ def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
let i ← maybeTee "wdIn.txt" false i let i ← maybeTee "wdIn.txt" false i
let o ← maybeTee "wdOut.txt" true o let o ← maybeTee "wdOut.txt" true o
let e ← maybeTee "wdErr.txt" true e let e ← maybeTee "wdErr.txt" true e
let state := {env := ← createEnv, game := `TestGame}
let initRequest ← i.readLspRequestAs "initialize" InitializeParams let initRequest ← i.readLspRequestAs "initialize" InitializeParams
o.writeLspResponse { o.writeLspResponse {
id := initRequest.id id := initRequest.id
@ -114,7 +115,6 @@ def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
: InitializeResult : InitializeResult
} }
} }
let state := {env := ← createEnv, game := `TestGame}
let context : ServerContext := { let context : ServerContext := {
hIn := i hIn := i
hOut := o hOut := o

@ -54,7 +54,7 @@ HiddenHint (a : ) (b : ) (c : ) (d : ) (h₁ : c = d) (h₂ : a = b)
"Auch eine Möglichkeit.. Kannst du das Goal so umschreiben, dass es mit einer Annahme übereinstimmt?" "Auch eine Möglichkeit.. Kannst du das Goal so umschreiben, dass es mit einer Annahme übereinstimmt?"
Hint (a : ) (b : ) (h : b = a) : a = b => Hint (a : ) (b : ) (h : b = a) : a = b =>
"Naja auch Umwege führen ans Ziel... Wenn du das Goal zu `a = A` umschreibst, kann man es mit "Naja auch Umwege führen ans Ziel... Wenn du das Goal zu `a = a` umschreibst, kann man es mit
`rfl` beweisen (rsp. das passiert automatisch.)" `rfl` beweisen (rsp. das passiert automatisch.)"
Hint (a : ) (b : ) (c : ) (d : ) (h₁ : c = d) (h₂ : d = b) (h₃ : d = a) : b = c => Hint (a : ) (b : ) (c : ) (d : ) (h₁ : c = d) (h₂ : d = b) (h₃ : d = a) : b = c =>

Loading…
Cancel
Save