Merge branch 'main' of github.com:leanprover-community/lean4game
This commit is contained in:
@@ -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])
|
||||||
|
}
|
||||||
|
|||||||
Generated
+3
-3
@@ -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",
|
||||||
|
|||||||
+1
-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",
|
||||||
|
|||||||
+29
-2
@@ -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 =>
|
||||||
|
|||||||
Reference in New Issue
Block a user