initalize repo

Co-authored-by: Patrick Massot PatrickMassot@users.noreply.github.com
pull/43/head
Alexander Bentkamp 4 years ago
commit 7563730292

3
.gitignore vendored

@ -0,0 +1,3 @@
node_modules
client/dist
server/build

@ -0,0 +1,73 @@
# Install docker
```
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release -y
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
sudo groupadd docker
sudo usermod -aG docker ${USER}
newgrp docker
```
# Install gVisor
```
sudo apt-get update && \
sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null
sudo apt-get update && sudo apt-get install -y runsc
sudo runsc install
sudo systemctl reload docker
```
# Install NPM
```
sudo apt-get install npm
sudo npm install -g http-server
```
# Clone NNG interface
```
(
git clone https://github.com/hhu-adam/nng4-interface.git
cd nng4-interface
rm package-lock.json
npm install
npm run build
)
```
```
git clone https://github.com/hhu-adam/NNG4.git
cd NNG4
docker rmi nng4:latest
docker build --pull --rm -f "Dockerfile" -t nng4:latest "."
```
# Start HTTP server
```
http-server ./nng4-interface/build
```
# Start WebSocket server
```
cd NNG4 && python3 ./gameserver.py
```
# Test docker
```
docker run --runtime=runsc --network=none --rm -it nng4:latest
```

@ -0,0 +1,9 @@
# Lean 4 Game
This is a prototype for a Lean 4 game platform. It is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
of Kevin Buzzard and Mohammad Pedramfar.
The project is currently mostly copied from Patrick Massot's [NNG4](https://github.com/PatrickMassot/NNG4), but we plan to extend it significantly.
Building this requires a [npm](https://www.npmjs.com/) toolchain. After cloning the repository you should run
`npm install` to pull in all dependencies. For development and experimentation, you can run `npm start` that will perform a non-optimized build and then run a local webserver on port 3000.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Lean 4 Game</title>
</head>
<body>
<div id="root"></div>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script src="../dist/bundle.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import { MathJaxContext } from "better-react-mathjax";
import useWebSocket from 'react-use-websocket';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { AppBar, CssBaseline, Toolbar, Typography } from '@mui/material';
import Welcome from './components/Welcome';
import Level from './components/Level';
import GoodBye from './components/GoodBye';
function App() {
const [title, setTitle] = useState("")
const [conclusion, setConclusion] = useState("")
const [levelTitle, setLevelTitle] = useState("")
const [nbLevels, setNbLevels] = useState(0)
const [curLevel, setCurLevel] = useState(0)
const [finished, setFinished] = useState(false)
const socketUrl = 'ws://' + window.location.hostname + ':8765'
const { sendJsonMessage, lastMessage, lastJsonMessage } = useWebSocket(socketUrl)
const mathJaxConfig = {
loader: {
load: ['input/tex-base', 'output/svg']
},
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']]
},
svg: {
fontCache: 'global'
}
}
function startGame() {
setCurLevel(1)
}
let mainComponent;
if (finished) {
mainComponent = <GoodBye message={conclusion} />
} else if (curLevel > 0) {
mainComponent = <Level sendJsonMessage={sendJsonMessage} lastMessage={lastMessage} lastJsonMessage={lastJsonMessage} nbLevels={nbLevels} level={curLevel} setCurLevel={setCurLevel} setLevelTitle={setLevelTitle} setFinished={setFinished}/>
} else {
mainComponent = <Welcome sendJsonMessage={sendJsonMessage} lastMessage={lastMessage} lastJsonMessage={lastJsonMessage} setNbLevels={setNbLevels} setTitle={setTitle} startGame={startGame} setConclusion={setConclusion}/>
}
return (
<div className="App">
<MathJaxContext config={mathJaxConfig}>
<CssBaseline />
<AppBar position="sticky" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
<Toolbar sx={{ justifyContent: "space-between" }}>
<Typography variant="h6" noWrap component="div">
{title}
</Typography>
<Typography variant="h6" noWrap component="div">
{levelTitle}
</Typography>
</Toolbar>
</AppBar>
{mainComponent}
</MathJaxContext>
</div>
)
}
export default App

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

@ -0,0 +1,23 @@
import React from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Box, Typography, Grid } from '@mui/material';
function GoodBye({ message }) {
return (<Grid container
direction="row"
justifyContent="center"
alignItems="center">
<Grid item xs={12} sm={6}>
<Box sx={{ m: 3 }}>
<Typography variant="body1" component="div">{message}</Typography>
</Box>
</Grid>
</Grid>)
}
export default GoodBye

@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Typography, Button, Paper, TextField, List, ListItem } from '@mui/material';
function InputZone({ index, history, messageOpen, setMessageOpen, completed, sendTactic, nbLevels, loadNextLevel, errors, lastTactic, undo, finishGame }) {
const [curInput, setCurInput] = useState("")
const inputRef = React.createRef()
const nextRef = React.createRef()
function handleCurInputChange(evt) { setCurInput(evt.target.value) }
function showPrevMessage() { setMessageOpen(true) }
useEffect(() => {
if (!messageOpen && !completed) inputRef.current.focus()
if (!messageOpen && completed && index < nbLevels) nextRef.current.focus()
if (!messageOpen && completed && index === nbLevels) finishGame()
}, [messageOpen, inputRef, nextRef, completed, finishGame, index, nbLevels])
async function submitForm(evt) {
evt.preventDefault(); // prevent app reloading on form submission
await sendTactic(curInput)
setCurInput("");
}
return (
<Paper sx={{
height: "100%", pt: 1, pl: 3, pr: 3, pb: 1,
justifyContent: "space-between", alignItems: "center", textAlign: "center"
}}
elevation={5}>
<Typography variant="h5">Invocation zone</Typography>
<List dense={true}>
{history.map((item, idx) => <ListItem key={idx}>{item}</ListItem>)}
</List>
<form onSubmit={submitForm}>
<TextField
id="outlined-input"
label="Invocation"
autoFocus={true}
inputRef={inputRef}
value={curInput}
onChange={handleCurInputChange}
/>
<br />
<Button type="submit" variant="contained" sx={{ mt: 2 }} disabled={errors.length > 0 || completed || curInput.trim() === ""} disableFocusRipple>Cast spell</Button>
<br />
<Button variant="text" onClick={undo} sx={{ ml: 3, mt: 2, mb: 2 }} disabled={lastTactic === "" ? true : false || completed}>Undo</Button>
{!messageOpen && <Button variant="text" onClick={showPrevMessage} sx={{ ml: 3, mt: 2, mb: 2 }}>See previous message</Button>}
<br />
{completed && index < nbLevels && <Button variant="contained" ref={nextRef} onClick={loadNextLevel} sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple>Go to Next Level</Button>}
</form>
</Paper>)
}
export default InputZone

@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { MathJax } from "better-react-mathjax";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Paper, Box, Typography, Accordion, AccordionSummary, AccordionDetails, Tabs, Tab } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
function TacticDoc(props) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{props.tactic.name}</Typography>
</AccordionSummary>
<AccordionDetails>
<MathJax>
<ReactMarkdown>{props.tactic.content}</ReactMarkdown>
</MathJax>
</AccordionDetails>
</Accordion>)
}
function LemmaDoc({ lemma }) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{lemma.userName}</Typography>
</AccordionSummary>
<AccordionDetails>
<MathJax>
<ReactMarkdown>{lemma.content}</ReactMarkdown>
</MathJax>
</AccordionDetails>
</Accordion>)
}
function LemmaDocs({ lemmas }) {
const [categories, setCategories] = useState(new Map())
const [curCategory, setCurCategory] = useState("")
useEffect(() => {
const cats = new Map()
lemmas.forEach(function (item) {
const category = item.category
cats.set(category, (cats.get(category) || []).concat([item]))
});
setCategories(cats)
setCurCategory(cats.keys().next().value)
}, [lemmas]);
return (
<Paper sx={{ px: 2, py: 1, mt: 2 }}>
<Typography variant="h5">Inventory</Typography>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={curCategory}
aria-label="Categories" variant="scrollable" scrollButtons="auto">
{(Array.from(categories)).map(([category, _]) => <Tab value={category} label={category} key={category} wrapped />)}
</Tabs>
</Box>
{curCategory && categories.get(curCategory).map((lemma) => <LemmaDoc lemma={lemma} key={lemma.name} />)}
</Paper>
)
}
function LeftPanel({ spells, inventory }) {
return (
<Box>
{spells.length > 0 &&
<Paper sx={{ px: 2, py: 1 }}>
<Typography variant="h5" sx={{ mb: 2 }}>Spell book</Typography>
{spells.map((spell) => <TacticDoc key={spell.name} tactic={spell} />)}
</Paper>}
{inventory.length > 0 && <LemmaDocs lemmas={inventory} />}
</Box>)
}
export default LeftPanel;

@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import Grid from '@mui/material/Unstable_Grid2';
import LeftPanel from './LeftPanel';
import InputZone from './InputZone';
import Message from './Message';
import TacticState from './TacticState';
function Level({ sendJsonMessage, lastMessage, lastJsonMessage, nbLevels, level, setCurLevel, setLevelTitle, setFinished }) {
const [index, setIndex] = useState(level) // Level number
const [tacticDocs, setTacticDocs] = useState([])
const [lemmaDocs, setLemmaDocs] = useState([])
const [leanData, setLeanData] = useState({})
const [history, setHistory] = useState([])
const [lastTactic, setLastTactic] = useState("")
const [errors, setErrors] = useState([])
const [message, setMessage] = useState("")
const [messageOpen, setMessageOpen] = useState(false)
const [completed, setCompleted] = useState(false)
// The next function will be called when the level changes
useEffect(() => {
sendJsonMessage({ "loadLevel": level });
}, [level, sendJsonMessage])
// The next function will be called when a message arrives or the level title changes
useEffect(() => {
console.log(lastMessage)
console.log(lastJsonMessage)
if ("nb_levels" in lastJsonMessage) { return } // this is an old message from starting the game
const data = lastJsonMessage;
if ("title" in data) { // This is the level metadata coming in
setLevelTitle("Level " + data["index"] + ": " + data["title"])
setIndex(parseInt(data["index"]))
setTacticDocs(data["tactics"])
setLemmaDocs(data["lemmas"])
}
if (data["message"] !== "" && data.errors.length === 0) {
setMessage(data["message"])
setMessageOpen(true)
}
setLeanData(data);
setErrors(data.errors);
if (data.goals.length === 0 && data.errors.length === 0) {
setCompleted(true)
}
}, [lastJsonMessage, lastMessage, setLevelTitle])
function sendTactic(input) {
sendJsonMessage({ "runTactic": input });
setLastTactic(input);
setHistory(history.concat([input]));
}
function undo() {
if (errors.length === 0) {
sendJsonMessage('undo');
}
if (history.length > 1) {
setLastTactic(history[history.length - 1]);
} else {
setLastTactic("");
};
setErrors([]);
setHistory(history.slice(0, -1));
}
function loadNextLevel() {
setCompleted(false)
setHistory([])
setCurLevel(index + 1)
}
function closeMessage() {
setMessageOpen(false)
}
function finishGame() {
setLevelTitle("")
setFinished(true)
}
return (
<Grid container sx={{ mt: 3, ml: 1, mr: 1 }} columnSpacing={{ xs: 1, sm: 2, md: 3 }}>
<Grid xs={4}>
<LeftPanel spells={tacticDocs} inventory={lemmaDocs} />
</Grid>
<Grid xs={4}>
<InputZone index={index} history={history} messageOpen={messageOpen} setMessageOpen={setMessageOpen} completed={completed} sendTactic={sendTactic} nbLevels={nbLevels} loadNextLevel={loadNextLevel}
errors={errors} lastTactic={lastTactic} undo={undo} finishGame={finishGame} />
<Message isOpen={messageOpen} content={message} close={closeMessage} />
</Grid>
<Grid xs={4}>
<TacticState goals={leanData.goals} errors={errors} lastTactic={lastTactic} completed={completed} />
</Grid>
</Grid>
)
}
export default Level

@ -0,0 +1,29 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import { MathJax } from "better-react-mathjax";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Button, Dialog, DialogContent, DialogContentText, DialogActions } from '@mui/material';
function Message({ isOpen, content, close }) {
return (
<Dialog open={isOpen} onClose={close}>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<MathJax><ReactMarkdown>{content}</ReactMarkdown></MathJax>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={close} autoFocus={true} disableFocusRipple>
Ok
</Button>
</DialogActions>
</Dialog>
)
}
export default Message

@ -0,0 +1,69 @@
import React from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import { Paper, Box, Typography } from '@mui/material';
const errorRegex = /<stdin>:1:(?<col>[^:]*): (?<msg>.*)/;
function Goal({ goal }) {
const hasObject = typeof goal.objects === "object" && goal.objects.length > 0
const hasAssumption = typeof goal.assumptions === "object" && goal.assumptions.length > 0
return (
<Box sx={{ pl: 2 }}>
{hasObject && <Box><Typography variant="h7">Objects</Typography>
<List>
{goal.objects.map((item) =>
<ListItem key={item[0]}>
<Typography color="primary" sx={{ mr: 1 }}>{item[0]}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item[1]}</Typography>
</ListItem>)}
</List></Box>}
{hasAssumption && <Box><Typography variant="h7">Assumptions</Typography>
<List>
{goal.assumptions.map((item) => <ListItem key={item}><Typography color="primary" sx={{ mr: 1 }}>{item[0]}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item[1]}</Typography></ListItem>)}
</List></Box>}
<Typography variant="h7">Prove:</Typography>
<Typography color="primary" sx={{ ml: 2 }}>{goal.goal}</Typography>
</Box>)
}
function TacticState({ goals, errors, lastTactic, completed }) {
const hasError = typeof errors === "object" && errors.length > 0
const hasGoal = typeof goals === "object" && goals.length > 0
const hasManyGoal = hasGoal && goals.length > 1
var col = ""
var msg = ""
if (hasError) {
const m = errors[0].match(errorRegex)
if (m) {
col = `Column ${m.groups.col}: `
msg = m.groups.msg
} else {
msg = errors[0]
if (msg === "Unrecognized tactic") { msg = "Unknown spell!" }
}
}
return (
<Box sx={{ height: "100%" }}>
{hasGoal && <Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, height: "100%" }}><Typography variant="h5">Current goal</Typography> <Goal goal={goals[0]} /></Paper>}
{completed && <Typography variant="h6">Level completed ! 🎉</Typography>}
{hasError && <Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, height: "100%" }}><Typography variant="h5" color="error">Spell invocation failed</Typography>
<Typography sx={{ my: 1 }}>{lastTactic}</Typography>
<Typography component="pre" sx={{ my: 1 }}>{col}{msg}</Typography>
<Typography>Use the undo button to go back to a sane state.</Typography>
</Paper>}
{hasManyGoal && <Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, mt: 1 }}>
<Typography variant="h6" sx={{ mb: 2 }}>Other goals</Typography>
{goals.slice(1).map((goal, index) => <Paper><Goal key={index} goal={goal} /></Paper>)}
</Paper>}
</Box>
)
}
export default TacticState

@ -0,0 +1,52 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { MathJax } from "better-react-mathjax";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Box, Typography, Button, CircularProgress, Grid } from '@mui/material';
function Welcome({ sendJsonMessage, lastJsonMessage, setNbLevels, setTitle, startGame, setConclusion }) {
const [leanData, setLeanData] = useState({})
// Will run at the very beginning
useEffect(() => {
sendJsonMessage('info')
}, [sendJsonMessage])
// Will run when a Json message arrives
useEffect(() => {
if (lastJsonMessage != null && lastJsonMessage.hasOwnProperty("nb_levels")) {
setLeanData(lastJsonMessage)
setNbLevels(lastJsonMessage.nb_levels)
setTitle(lastJsonMessage.title)
document.title = lastJsonMessage.title
setConclusion(lastJsonMessage.conclusion)
}
}, [lastJsonMessage, setNbLevels, setTitle, setConclusion])
let content
if ("introduction" in leanData) {
content = (<Box sx={{ m: 3 }}>
<Typography variant="body1" component="div">
<MathJax>
<ReactMarkdown>{leanData["introduction"]}</ReactMarkdown>
</MathJax>
</Typography>
<Box textAlign='center' sx={{ m: 5 }}>
<Button onClick={startGame} variant="contained">Start rescue mission</Button>
</Box>
</Box>)
} else {
content = <Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}><CircularProgress /></Box>
}
return <Grid container direction="row" justifyContent="center" alignItems="center">
<Grid item xs={12} sm={6}>{content}</Grid>
</Grid>
}
export default Welcome

@ -0,0 +1,11 @@
body {
margin: 0;
font-family: Roboto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: Roboto Mono;
color: rgba(0, 32, 90, 0.87);
}

@ -0,0 +1,11 @@
import React from 'react'
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
self.$RefreshReg$ = () => {};
self.$RefreshSig$ = () => () => {};
const container = document.getElementById('root');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<App />);

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

22853
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,67 @@
{
"name": "lean4-game",
"version": "0.1.0",
"private": true,
"homepage": ".",
"dependencies": {
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fontsource/roboto": "^4.5.8",
"@fontsource/roboto-mono": "^4.5.8",
"@mui/icons-material": "^5.10.6",
"@mui/material": "^5.10.9",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.8",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"better-react-mathjax": "^2.0.2",
"concurrently": "^7.4.0",
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.3",
"react-refresh": "^0.14.0",
"react-use-websocket": "^4.2.0",
"web-vitals": "^3.0.3",
"ws": "^8.9.0"
},
"scripts": {
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
"start_server": "cd server && node ./index.js",
"start_client": "webpack-dev-server --env.NODE_ENV=development --hot",
"build": "npm run build_server && npm run build_client",
"build_server": "cd server && lake build",
"build_client": "webpack --env.NODE_ENV=production"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/cli": "^7.1.0",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.2",
"css-loader": "^1.0.0",
"file-loader": "^6.2.0",
"style-loader": "^0.23.0",
"webpack": "^4.19.1",
"webpack-cli": "^3.1.1",
"webpack-dev-server": "^3.1.8"
}
}

2
server/.gitignore vendored

@ -0,0 +1,2 @@
build
node_modules

@ -0,0 +1,25 @@
FROM ubuntu:18.04
WORKDIR /
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y git curl
# Install elan
RUN curl -sSfL https://github.com/leanprover/elan/releases/download/v1.4.2/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
RUN ./elan-init -y --default-toolchain leanprover/lean4:stable
ENV PATH="${PATH}:/root/.elan/bin"
# clone repos
RUN git clone https://github.com/hhu-adam/lean4-game-server.git game-server
RUN cd game-server && git checkout gvisor
RUN cd game-server && lake build
RUN git clone https://github.com/hhu-adam/NNG4.git
WORKDIR /NNG4
RUN git checkout gvisor
RUN lake build
CMD ["./build/bin/nng"]

@ -0,0 +1,13 @@
import NNG.GameServer.Server
import NNG.NNG
def System.FilePath.parent! (fp : System.FilePath) : System.FilePath :=
match fp.parent with
| some path => path
| none => panic! "Couldn't find parent folder"
unsafe def main : IO Unit := do
let build_folder := (← IO.appPath).parent!.parent!
let paths : List System.FilePath := [build_folder/"lib",
(← Lean.findSysroot) / "lib" / "lean"]
Server.runGame `NNG paths

@ -0,0 +1,229 @@
import Lean
import NNG.GameServer.Utils
import NNG.GameServer.EnvExtensions
open Lean Meta
set_option autoImplicit false
/-! ## Easy metadata -/
section metadata
open Lean Meta Elab Command Term
/-- Create a game with the given identifier as name. -/
elab "Game" n:str : command => do
gameExt.set {name := n.getString}
/-- Define the current level number. -/
elab "Level" n:num : command => do
let idx := n.getNat
setCurLevelIdx idx
levelsExt.insert idx {index := idx}
/-- Define the title of the current game or current level if some
building a level. -/
elab "Title" t:str : command => do
let lvlIdx ← getCurLevelIdx
if lvlIdx > 0 then
let some lvl := (← levelsExt.find? lvlIdx) | throwError "Unable to find level"
levelsExt.update lvlIdx {lvl with title := t.getString}
else
gameExt.set {← gameExt.get with title := t.getString}
/-- Define the introduction of the current game or current level if some
building a level. -/
elab "Introduction" t:str : command => do
let lvlIdx ← getCurLevelIdx
if lvlIdx > 0 then
let some lvl := (← levelsExt.find? lvlIdx) | throwError "Unable to find level"
levelsExt.update lvlIdx {lvl with introduction := t.getString}
else
gameExt.set {← gameExt.get with introduction := t.getString}
/-- Define the statement of the current level. -/
elab "Statement" sig:declSig val:declVal : command => do
let lvlIdx ← getCurLevelIdx
let declName : Name := (← gameExt.get).name ++ ("level" ++ toString lvlIdx : String)
elabCommand (← `(theorem $(mkIdent declName) $sig $val))
let (binders, _) := expandDeclSig sig
let mut nb : Nat := 0
for arg in binders.getArgs do
nb := nb + arg[1].getArgs.size
let some cInfo := (← getEnv).find? declName | throwError "Declaration not found"
levelsExt.update lvlIdx {← getCurLevel with goal := cInfo.type, intro_nb := nb}
/-- Define the conclusion of the current game or current level if some
building a level. -/
elab "Conclusion" t:str : command => do
let lvlIdx ← getCurLevelIdx
if lvlIdx > 0 then
let some lvl := (← levelsExt.find? lvlIdx) | throwError "Unable to find level"
levelsExt.update lvlIdx {lvl with conclusion := t.getString}
else
gameExt.set {← gameExt.get with conclusion := t.getString}
/-- Print current game for debugging purposes. -/
elab "PrintCurGame" : command => do
logInfo (repr (← gameExt.get))
/-- Print current level for debugging purposes. -/
elab "PrintCurLevel" : command => do
match ← levelsExt.find? (← getCurLevelIdx) with
| some lvl => logInfo (repr lvl)
| none => logInfo "Could not find level"
/-- Print levels for debugging purposes. -/
elab "PrintLevels" : command => do
logInfo $ repr $ (levelsExt.getState (← getEnv)).toList.map (·.fst)
end metadata
/-! ## Messages -/
open Lean Meta Elab Command Term
declare_syntax_cat mydecl
syntax "(" ident ":" term ")" : mydecl
def getIdent : TSyntax `mydecl → Ident
| `(mydecl| ($n:ident : $_t:term)) => n
| _ => default
def getType : TSyntax `mydecl → Term
| `(mydecl| ($_n:ident : $t:term)) => t
| _ => default
/-- From a term `s` and a list of pairs `(i, t) ; Ident × Term`, create the syntax
where `s` is preceded with universal quantifiers `∀ i : t`. -/
def mkGoalSyntax (s : Term) : List (Ident × Term) → MacroM Term
| (n, t)::tail => do return (← `(∀ $n : $t, $(← mkGoalSyntax s tail)))
| [] => return s
/-- Declare a message. This version doesn't prevent the unused linter variable from running. -/
local elab "Message'" decls:mydecl* ":" goal:term "=>" msg:str : command => do
let g ← liftMacroM $ mkGoalSyntax goal (decls.map (λ decl => (getIdent decl, getType decl))).toList
let g ← liftTermElabM do (return ← instantiateMVars (← elabTerm g none))
let (ctx_size, normalized_goal) ← liftTermElabM do
let msg_mvar ← mkFreshExprMVar g MetavarKind.syntheticOpaque
msg_mvar.mvarId!.withContext do
let (_, msg_mvar) ← msg_mvar.mvarId!.introNP decls.size
return ((← msg_mvar.getDecl).lctx.size, (← normalizedRevertExpr msg_mvar))
let lvlIdx ← getCurLevelIdx
let lvl ← getCurLevel
levelsExt.update lvlIdx {lvl with messages := lvl.messages.push {
ctx_size := ctx_size,
normalized_goal := normalized_goal,
intro_nb := decls.size,
message := msg.getString }}
/-- Declare a message in reaction to a given tactic state in the current level. -/
macro "Message" decls:mydecl* ":" goal:term "=>" msg:str : command => do
`(set_option linter.unusedVariables false in Message' $decls* : $goal => $msg)
/-! ## Tactics -/
/-- Declare a documentation entry for some tactic.
Expect an identifier and then a string literal. -/
elab "TacticDoc" name:ident content:str : command =>
modifyEnv (tacticDocExt.addEntry · {
name := name.getId,
content := content.getString })
/-- Declare a set of tactic documentation entries.
Expect an identifier used as the set name then `:=` and a
space separated list of identifiers.
-/
elab "TacticSet" name:ident ":=" args:ident* : command => do
let docs := tacticDocExt.getState (← getEnv)
let mut entries : Array TacticDocEntry := #[]
for arg in args do
let name := arg.getId
match docs.find? (·.name = name) with
| some doc => entries := entries.push doc
| none => throwError "Documentation for tactic {name} wasn't found."
modifyEnv (tacticSetExt.addEntry · {
name := name.getId,
tactics := entries })
instance : Quote TacticDocEntry `term :=
⟨λ entry => Syntax.mkCApp ``TacticDocEntry.mk #[quote entry.name, quote entry.content]⟩
/-- Declare the list of tactics that will be displayed in the current level.
Expects a space separated list of identifiers that refer to either a tactic doc
entry or a tactic doc set. -/
elab "Tactics" args:ident* : command => do
let env ← getEnv
let docs := tacticDocExt.getState env
let sets := tacticSetExt.getState env
let mut tactics : Array TacticDocEntry := #[]
for arg in args do
let name := arg.getId
match docs.find? (·.name = name) with
| some entry => tactics := tactics.push entry
| none => match sets.find? (·.name = name) with
| some entry => tactics := tactics ++ entry.tactics
| none => throwError "Tactic doc or tactic set {name} wasn't found."
let lvlIdx ← getCurLevelIdx
if lvlIdx > 0 then
let some lvl := (← levelsExt.find? lvlIdx) | throwError "Unable to find level"
levelsExt.update lvlIdx {lvl with tactics := tactics}
else
throwError "This command can be used only while building a level."
/-! ## Lemmas -/
/-- Declare a documentation entry for some lemma.
Expect two identifiers and then a string literal. The first identifier is meant
as the real name of the lemma while the second is the displayed name. Currently
the real name isn't used. -/
elab "LemmaDoc" name:ident "as" userName:ident "in" category:str content:str : command =>
modifyEnv (lemmaDocExt.addEntry · {
name := name.getId,
userName := userName.getId,
category := category.getString,
content := content.getString })
/-- Declare a set of lemma documentation entries.
Expect an identifier used as the set name then `:=` and a
space separated list of identifiers. -/
elab "LemmaSet" name:ident ":" title:str ":=" args:ident* : command => do
let docs := lemmaDocExt.getState (← getEnv)
let mut entries : Array LemmaDocEntry := #[]
for arg in args do
let name := arg.getId
match docs.find? (·.userName = name) with
| some doc => entries := entries.push doc
| none => throwError "Lemma doc {name} wasn't found."
modifyEnv (lemmaSetExt.addEntry · {
name := name.getId,
title := title.getString,
lemmas := entries })
instance : Quote LemmaDocEntry `term :=
⟨λ entry => Syntax.mkCApp ``LemmaDocEntry.mk #[quote entry.name, quote entry.userName, quote entry.category, quote entry.content]⟩
/-- Declare the list of lemmas that will be displayed in the current level.
Expects a space separated list of identifiers that refer to either a lemma doc
entry or a lemma doc set. -/
elab "Lemmas" args:ident* : command => do
let env ← getEnv
let docs := lemmaDocExt.getState env
let sets := lemmaSetExt.getState env
let mut lemmas : Array LemmaDocEntry := #[]
for arg in args do
let name := arg.getId
match docs.find? (·.userName = name) with
| some entry => lemmas := lemmas.push entry
| none => match sets.find? (·.name = name) with
| some entry => lemmas := lemmas ++ entry.lemmas
| none => throwError "Lemma doc or lemma set {name} wasn't found."
let lvlIdx ← getCurLevelIdx
if lvlIdx > 0 then
let some lvl := (← levelsExt.find? lvlIdx) | throwError "Unable to find level"
levelsExt.update lvlIdx {lvl with lemmas := lemmas}
else
throwError "This command can be used only while building a level."

@ -0,0 +1,151 @@
import NNG.GameServer.HashMapExtension
import NNG.GameServer.SingleValPersistentEnvExtension
/-! # Environment extensions
The game framework stores almost all its game building data in environment extensions
defined in this file. MAyn of them are `SimplePersistentEnvExtension` but we also
use `HashMapExtension` and `SingleValPersistentEnvExtension`
-/
open Lean
/-! ## Messages -/
structure GoalMessageEntry where
ctx_size : Nat
normalized_goal : Expr
intro_nb : Nat
message : String
deriving Repr
/-! ## Tactic documentation -/
structure TacticDocEntry where
name : Name
content : String
deriving ToJson, Repr
/-- Environment extension for tactic documentation. -/
initialize tacticDocExt : SimplePersistentEnvExtension TacticDocEntry (Array TacticDocEntry) ←
registerSimplePersistentEnvExtension {
name := `tactic_doc
addEntryFn := Array.push
addImportedFn := Array.concatMap id
}
open Elab Command in
/-- Print a registered tactic doc for debugging purposes. -/
elab "#print_tactic_doc" : command => do
for entry in tacticDocExt.getState (← getEnv) do
dbg_trace "{entry.name} : {entry.content}"
structure TacticSetEntry where
name : Name
tactics : Array TacticDocEntry
deriving ToJson, Repr
/-- Environment extension for tactic sets. -/
initialize tacticSetExt : SimplePersistentEnvExtension TacticSetEntry (Array TacticSetEntry) ←
registerSimplePersistentEnvExtension {
name := `tactic_set
addEntryFn := Array.push
addImportedFn := Array.concatMap id
}
open Elab Command in
/-- Print all registered tactic sets for debugging purposes. -/
elab "#print_tactic_set" : command => do
for entry in tacticSetExt.getState (← getEnv) do
dbg_trace "{entry.name} : {entry.tactics.map TacticDocEntry.name}"
/-! ## Lemma documentation -/
structure LemmaDocEntry where
name : Name
userName : Name
category : String
content : String
deriving ToJson, Repr
/-- Environment extension for lemma documentation. -/
initialize lemmaDocExt : SimplePersistentEnvExtension LemmaDocEntry (Array LemmaDocEntry) ←
registerSimplePersistentEnvExtension {
name := `lemma_doc
addEntryFn := Array.push
addImportedFn := Array.concatMap id
}
open Elab Command in
/-- Print a lemma doc for debugging purposes. -/
elab "#print_lemma_doc" : command => do
for entry in lemmaDocExt.getState (← getEnv) do
dbg_trace "{entry.userName} ({entry.name}) in {entry.category}: {entry.content}"
structure LemmaSetEntry where
name : Name
title : String
lemmas : Array LemmaDocEntry
deriving ToJson, Repr
/-- Environment extension for lemma sets. -/
initialize lemmaSetExt : SimplePersistentEnvExtension LemmaSetEntry (Array LemmaSetEntry) ←
registerSimplePersistentEnvExtension {
name := `lemma_set
addEntryFn := Array.push
addImportedFn := Array.concatMap id
}
open Elab Command in
/-- Print all registered lemma sets for debugging purposes. -/
elab "#print_lemma_set" : command => do
for entry in lemmaSetExt.getState (← getEnv) do
dbg_trace "{entry.name} : {entry.lemmas.map LemmaDocEntry.name}"
/-! ## Game -/
structure Game where
name : Name
title : String := ""
introduction : String := ""
conclusion : String := ""
authors : List String := []
nb_levels : Nat := 0
deriving Repr, Inhabited, ToJson
initialize gameExt : SingleValPersistentEnvExtension Game ← registerSingleValPersistentEnvExtension `gameExt Game
/-! ## Levels -/
/- Register a (non-persistent) environment extension to hold the current level number. -/
initialize curLevelExt : EnvExtension Nat ← registerEnvExtension (pure 0)
variable {m: Type → Type} [Monad m] [MonadEnv m]
def setCurLevelIdx (lvl : Nat) : m Unit :=
modifyEnv (curLevelExt.setState · lvl)
def getCurLevelIdx : m Nat := do
return curLevelExt.getState (← getEnv)
structure GameLevel where
index: Nat
title: String := default
introduction: String := default
conclusion: String := default
tactics: Array TacticDocEntry := default
lemmas: Array LemmaDocEntry := default
messages: Array GoalMessageEntry := default
goal : Expr := default
intro_nb : Nat := default
deriving Inhabited, Repr
initialize levelsExt : HashMapExtension Nat GameLevel ← mkHashMapExtension `levels Nat GameLevel
def getCurLevel [MonadError m] : m GameLevel := do
let idx ← getCurLevelIdx
match (← levelsExt.find? idx) with
| some level => return level
| none => throwError "Couldn't find level {idx}"

@ -0,0 +1,31 @@
import Lean
open Lean Std
def HashMapExtension (α β : Type) [BEq α] [Hashable α] := SimplePersistentEnvExtension (α × β) (HashMap α β)
instance (α β : Type) [BEq α] [Hashable α] : Inhabited (HashMapExtension α β) :=
inferInstanceAs (Inhabited (SimplePersistentEnvExtension (α × β) (HashMap α β)))
def mkHashMapExtension (name : Name) (α β : Type) [BEq α] [Hashable α] : IO (HashMapExtension α β) :=
registerSimplePersistentEnvExtension {
name := name,
addImportedFn := mkStateFromImportedEntries (λ s n => s.insert n.1 n.2) {},
addEntryFn := (λ s n => s.insert n.1 n.2),
toArrayFn := fun es => es.toArray
}
namespace HashMapExtension
variable {α β : Type} [BEq α] [Hashable α] {m: Type → Type} [Monad m] [MonadEnv m]
def find? (ext : HashMapExtension α β) (a : α) : m $ Option β := do
return (ext.getState (← getEnv)).find? a
def insert (ext : HashMapExtension α β) (a : α) (b : β) : m Unit :=
modifyEnv (ext.addEntry · (a, b))
def update (ext : HashMapExtension α β) (a : α) (b : β) : m Unit :=
modifyEnv (ext.addEntry · (a, b))
end HashMapExtension

@ -0,0 +1,243 @@
/-
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 NNG.GameServer.Utils
import NNG.GameServer.EnvExtensions
open Lean Meta Elab Tactic Std
/- Convert JSON to string without line breaks -/
-- TODO: this is too slow...
instance instToStringJsonOneLine : ToString Json := ToString.mk (fun o => (toString o).replace "\n" "")
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 := "<Game>",
fileMap := { source := "", positions := #[0], lines := #[1] } }
partial def runLevel (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 }
output (← levelInfo.toJson)
mainLoop
levelM.run lvl |>.run' state
let metaM : MetaM Unit := termElabM.run' (ctx := {})
try
let env ← importModules [{ module := `Init : Import }, { module := GameName ++ "Levels" ++ levelName : Import }] {} 0
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 "<stdin>" 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
match ← Action.get with
| Action.runTactic tac => do let resp ← runTactic tac; output s!"{← resp.toJson}"
| Action.loadLevel n => runLevel GameName levels n
| Action.undo => do modify fun s => s.pop; output s!"{← (← mkResponse).toJson}"
| Action.restartLevel => runLevel GameName levels idx
| Action.prev => runLevel GameName levels idx.pred
| Action.next => runLevel GameName levels idx.succ
| Action.quit => IO.Process.exit 0
| Action.restartGame => output "Can't restart game now"
| Action.info => output "Can't get info now"
| Action.invalid s => output s!"{← { ← mkResponse with errors := #[s!"Invalid action: {s}"] : Response}.toJson}"
mainLoop
open System Lean Std in
partial def runGame (GameName : Name) (paths : List FilePath): IO Unit := do
searchPathRef.set paths
let env ← importModules [{ module := `Init : Import }, { module := GameName ++ GameName : Import }] {} 0
let termElabM : TermElabM Unit := do
let levels := levelsExt.getState env
let game := {← gameExt.get with nb_levels := levels.size }
mainLoop game levels
let metaM : MetaM Unit := termElabM.run' (ctx := {})
discard <| metaM.run'.toIO coreCtx { env := env }
where
mainLoop (game : Game) (levels : HashMap Nat GameLevel): IO Unit := do
match ← Action.get with
| Action.info => output (toJson game)
| Action.loadLevel n => runLevel GameName levels n
| Action.quit => IO.Process.exit 0
| Action.invalid s => output s!"Invalid action: {s}"
| _ => output "Invalid action"
mainLoop game levels
end Server

@ -0,0 +1,26 @@
import Lean
open Lean
/-- A persistent environment extension that is meant to hold a single (mutable) value. -/
def SingleValPersistentEnvExtension (α : Type) := PersistentEnvExtension α α α
instance {α} [Inhabited α] : Inhabited (SingleValPersistentEnvExtension α) :=
inferInstanceAs <| Inhabited <| PersistentEnvExtension α α α
def registerSingleValPersistentEnvExtension (name : Name) (α : Type) [Inhabited α] : IO (SingleValPersistentEnvExtension α) :=
registerPersistentEnvExtension {
name := name,
mkInitial := pure default,
addImportedFn := mkStateFromImportedEntries (fun _ b => return b) (return default),
addEntryFn := (λ _ b => b),
exportEntriesFn := λ x => #[x]
}
variable {m: Type → Type} [Monad m] [MonadEnv m] {α : Type} [Inhabited α]
def SingleValPersistentEnvExtension.get (ext : SingleValPersistentEnvExtension α) : m α :=
return ext.getState (← getEnv)
def SingleValPersistentEnvExtension.set (ext : SingleValPersistentEnvExtension α) (a : α) : m Unit := do
modifyEnv (ext.modifyState · (λ _ => a))

@ -0,0 +1,30 @@
import Lean
open Lean
def Lean.Expr.getFVars (e : Expr) : Array FVarId :=
(Lean.collectFVars {} e).fvarIds
/-- Returns the type of the goal after reverting all free variables in the order
where they appear in the goal type. -/
partial def normalizedRevertExpr (goal : MVarId) : MetaM Expr := do
goal.withContext do
let (_, new) ← goal.revert (← goal.getType).getFVars true
let e ← new.getType
if e.hasFVar then
return ← normalizedRevertExpr new
else
return (← new.getType)
def Lean.MessageLog.getErrorMessages (log : MessageLog) : MessageLog :=
{ msgs := log.msgs.filter (·.severity matches .error) }
/-- A version of `println!` that actually does its job by flushing stdout. -/
def output {α : Type} [ToString α] (s : α) : IO Unit := do
println! s
IO.FS.Stream.flush (← IO.getStdout)
def Lean.LocalDecl.toJson (decl : LocalDecl) : MetaM Json :=
return Lean.ToJson.toJson [toString decl.userName, toString (← Meta.ppExpr decl.type)]

@ -0,0 +1,14 @@
import NNG.GameServer.Commands
import NNG.MyNat
LemmaDoc zero_add as zero_add in "Addition"
"This lemma says `∀ a : , 0 + a = a`."
LemmaDoc add_zero as add_zero in "Addition"
"This lemma says `∀ a : , a + 0 = a`."
LemmaDoc add_succ as add_succ in "Addition"
"This lemma says `∀ a b : , a + succ b = succ (a + b)`."
LemmaSet addition : "Addition lemmas" :=
zero_add add_zero

@ -0,0 +1,26 @@
import NNG.Metadata
Level 1
Title "The reflexivity spell"
Introduction
"
Let's learn a first spell: the `rfl` spell. `rfl` stands for \"reflexivity\", which is a fancy
way of saying that it will prove any goal of the form `A = A`. It doesn't matter how
complicated `A` is, all that matters is that the left hand side is *exactly equal* to the
right hand side (a computer scientist would say \"definitionally equal\"). I really mean
\"press the same buttons on your computer in the same order\" equal.
For example, `x * y + z = x * y + z` can be proved by `rfl`, but `x + y = y + x` cannot.
This is a very low level spell, but you need to start somewhere.
After closing this message, type rfl in the invocation zone and hit Enter or click
the \"Cast spell\" button.
"
Statement (x y z : ) : x * y + z = x * y + z := by
rfl
Conclusion "Congratulations for completing your first level! You can now click on the *Go to next level* button."
Tactics rfl

@ -0,0 +1,36 @@
import NNG.Metadata
Level 2
Title "The rewriting spell"
Introduction
"
The rewrite spell is the way to \"substitute in\" the value
of an expression. In general, if you have a hypothesis of the form `A = B`, and your
goal mentions the left hand side `A` somewhere, then
the `rewrite` tactic will replace the `A` in your goal with a `B`.
The documentation for `rewrite` just appeared in your spell book.
Play around with the menus and see what is there currently.
More information will appear as you progress.
Take a look in the top right box at what we have.
The variables $x$ and $y$ are natural numbers, and we have
an assumption `h` that $y = x + 7$. Our goal
is to prove that $2y=2(x+7)$. This goal is obvious -- we just
substitute in $y = x+7$ and we're done. In Lean, we do
this substitution using the `rewrite` spell. This spell takes a list of equalities
or equivalences so you can cast `rewrite [h]`.
"
Statement (x y : ) (h : y = x + 7) : 2 * y = 2 * (x + 7) := by
rewrite [h]
rfl
Message (x : ) (y : ) (h : y = x + 7) : 2*(x + 7) = 2*(x + 7) =>
"Great! Now the goal should be easy to reach using the `rfl` spell."
Conclusion "Congratulations for completing your second level!"
Tactics rfl rewrite

@ -0,0 +1,81 @@
import NNG.Metadata
Level 3
Title "Peano's axioms"
Introduction
"
The team that salvaged the type `` of natural numbers actually got us three things:
* a term `0 : `, interpreted as the number zero.
* a function `succ : `, with `succ n` interpreted as \"the number after $n$\".
* The principle of mathematical induction.
These are essentially the axioms isolated by Peano which uniquely characterise
the natural numbers (we also need recursion, but we can ignore it for now).
The first axiom says that $0$ is a natural number. The second says that there
is a `succ` function which eats a number and spits out the number after it,
so $\\operatorname{succ}(0)=1$, $\\operatorname{succ}(1)=2$ and so on.
Peano's last axiom is the principle of mathematical induction. This is a deeper
fact. It says that if we have infinitely many true/false statements $P(0)$, $P(1)$,
$P(2)$ and so on, and if $P(0)$ is true, and if for every natural number $d$
we know that $P(d)$ implies $P(\\operatorname{succ}(d))$, then $P(n)$ must be true for every
natural number $n$. It's like saying that if you have a long line of dominoes, and if
you knock the first one down, and if you know that if a domino falls down then the one
after it will fall down too, then you can deduce that all the dominos will fall down.
One can also think of it as saying that every natural number
can be built by starting at `0` and then applying `succ` a finite number of times.
Peano's insights were firstly that these axioms completely characterise
the natural numbers, and secondly that these axioms alone can be used to build
a whole bunch of other structure on the natural numbers, for example
addition, multiplication and so on.
This game is all about seeing how far these axioms of Peano can take us.
Let's practice our use of the `rewrite` tactic in the following example.
Our hypothesis `h` is a proof that `succ(a) = b` and we want to prove that
`succ(succ(a))=succ(b)`. In words, we're going to prove that if
`b` is the number after `a` then `succ(b)` is the number after `succ(a)`.
Note that the system drops brackets when they're not
necessary, so `succ b` just means `succ(b)`.
Now here's a tricky question. Knowing that our goal is `succ (succ a) = succ b`,
and our assumption is `h : succ a = b`, then what will the goal change
to when we type
`rewrite [h]`
and hit enter? Remember that `rewrite [h]` will
look for the *left* hand side of `h` in the goal, and will replace it with
the *right* hand side. Try and figure out how the goal will change, and
then try it.
"
Statement (a b : ) (h : succ a = b) : succ (succ a) = succ b := by
rewrite [h]
rfl
Message (a : ) (b : ) (h : succ a = b) : succ b = succ b =>
"
Look: Lean changed `succ a` into `b`, so the goal became `succ b = succ b`.
That goal is of the form `X = X`, so you know what to do.
"
Conclusion "Congratulations for completing the third level!
You may be wondering whether we could have just substituted in the definition of `b`
and proved the goal that way. To do that, we would want to replace the right hand
side of `h` with the left hand side. You do this in Lean by writing `rewrite [<- h]`. You get the
left-arrow by typing `\\l` and then a space; note that this is a small letter L,
not a number 1. You can just edit your proof and try it.
You may also be wondering why we keep writing `succ(b)` instead of `b+1`. This
is because we haven't defined addition yet! On the next level, the final level
of the tutorial, we will introduce addition, and then
we'll be ready to enter Addition World.
"
Tactics rfl rewrite

@ -0,0 +1,62 @@
import NNG.Metadata
Level 4
Title "Addition"
Introduction
"
Peano defined addition `a + b` by induction on `b`, or,
more precisely, by *recursion* on `b`. He first explained how to add 0 to a number:
this is the base case.
* `add_zero (a : ) : a + 0 = a`
We will call this theorem `add_zero`. It has just appeared in your inventory!
Mathematicians sometimes call it \"Lemma 2.1\" or \"Hypothesis P6\" or something. But
computer scientists call it `add_zero` because it tells you
what the answer to \"$x$ add zero\" is. It's a *much* better name than \"Lemma 2.1\".
Even better, we can use the rewrite tactic with `add_zero`.
If you ever see `x + 0` in your goal, `rewrite [add_zero]` will simplify it to `x`.
This is because `add_zero` is a proof that `x + 0 = x` (more precisely,
`add_zero x` is a proof that `x + 0 = x` but Lean can figure out the `x` from the context).
Now here's the inductive step. If you know how to add `d` to `a`, then
Peano tells you how to add `succ(d)` to `a`. It looks like this:
* `add_succ (a d : ) : a + succ(d) = succ (a + d)`
What's going on here is that we assume `a + d` is already
defined, and we define `a + succ(d)` to be the number after it.
This is also in your inventory now -- `add_succ` tells you
how to add a successor to something. If you ever see `... + succ ...`
in your goal, you should be able to use `rewrite [add_succ]` to make
progress. Here is a simple example where we shall see both. Let's prove
that $x$ add the number after $0$ is the number after $x$.
Observe that the goal mentions `... + succ ...`. So type
`rewrite [add_succ]`
and hit enter; see the goal change.
"
Statement (a : ) : a + succ 0 = succ a := by
rewrite [add_succ]
rewrite [add_zero]
rfl
Message (a : ) : succ (a + 0) = succ a => "
Do you see that the goal now mentions ` ... + 0 ...`? So type
`rewrite [add_zero]`
and try to finish the level alone from there.
"
Conclusion "Congratulations for completing your fourth level! This is the end of the tutorial part
of the game. Serious things start in the next level."
Tactics rfl rewrite
Lemmas add_succ add_zero

@ -0,0 +1,125 @@
import NNG.Metadata
import NNG.Tactics
Level 5
Title "The induction_on spell"
Introduction
"
Welcome to Addition World. If you've done all four levels in tutorial world
and know about `rewrite` and `rfl`, then you're in the right place. Here's
a reminder of the things you're now equipped with which we'll need in this world.
## Data:
* a type called ``
* a term `0 : `, interpreted as the number zero.
* a function `succ : `, with `succ n` interpreted as \"the number after `n`\".
* Usual numerical notation 0,1,2 etc (although 2 onwards will be of no use to us until much later ;-) ).
* Addition (with notation `a + b`).
## Theorems:
* `add_zero (a : ) : a + 0 = a`. Use with `rewrite [add_zero]`.
* `add_succ (a b : ) : a + succ(b) = succ(a + b)`. Use with `rewrite [add_succ]`.
* The principle of mathematical induction. Use with `induction_on` (see below)
## Spells:
* `rfl` : proves goals of the form `X = X`
* `rewrite [h]` : if h is a proof of `A = B`, changes all A's in the goal to B's.
* `induction_on n with d hd` : we're going to learn this right now.
# Important thing:
This is a *really* good time to check you understand about the spell book and the inventory on
the left. Eveything you need is collected in those lists. They
will prove invaluable as the number of theorems we prove gets bigger. On the other hand,
we only need to learn one more spell to really start going places, so let's learn about
that spell right now.
OK so let's see induction in action. We're going to prove
`zero_add (n : ) : 0 + n = n`.
That is: for all natural numbers $n$, $0+n=n$. Wait $-$ what is going on here?
Didn't we already prove that adding zero to $n$ gave us $n$?
No we didn't! We proved $n + 0 = n$, and that proof was called `add_zero`. We're now
trying to establish `zero_add`, the proof that $0 + n = n$. But aren't these two theorems
the same? No they're not! It is *true* that `x + y = y + x`, but we haven't
*proved* it yet, and in fact we will need both `add_zero` and `zero_add` in order
to prove this. In fact `x + y = y + x` is the boss level for addition world,
and `induction_on` is the only other spell you'll need to beat it.
Now `add_zero` is one of Peano's axioms, so we don't need to prove it, we already have it
(indeed, if you've opened the Addition World theorem statements on the left, you can even see it).
To prove `0 + n = n` we need to use induction on $n$. While we're here,
note that `zero_add` is about zero add something, and `add_zero` is about something add zero.
The names of the proofs tell you what the theorems are. Anyway, let's prove `0 + n = n`.
Start by casting `induction_on n`.
"
Statement (n : ) : 0 + n = n := by
induction_on n
rewrite [add_zero]
rfl
rewrite [add_succ]
rewrite [ind_hyp]
rfl
Message : (0 : ) + 0 = 0 => "
We now have *two goals!* The
induction spell has generated for us a base case with `n = 0` (the goal at the top)
and an inductive step (the goal underneath). The golden rule: **spells operate on the current goal** --
the goal at the top. So let's just worry about that top goal now, the base case `0 + 0 = 0`.
Remember that `add_zero` (the proof we have already) is the proof of `x + 0 = x`
(for any $x$) so we can try
`rewrite [add_zero]`
What do you think the goal will change to? Remember to just keep
focussing on the top goal, ignore the other one for now, it's not changing
and we're not working on it.
"
Message (n : ) (ind_hyp: 0 + n = n) : 0 + succ n = succ n =>
"
Great! You solved the base case. We are now be back down
to one goal -- the inductive step.
We have a fixed natural number `n`, and the inductive hypothesis `ind_hyp : 0 + n = n`
saying that we have a proof of `0 + n = n`.
Our goal is to prove `0 + succ n = succ n`. In words, we're showing that
if the lemma is true for `n`, then it's also true for the number after `n`.
That's the inductive step. Once we've proved this inductive step, we will have proved
`zero_add` by the principle of mathematical induction.
To prove our goal, we need to use `add_succ`. We know that `add_succ 0 n`
is the result that `0 + succ n = succ (0 + n)`, so the first thing
we need to do is to replace the left hand side `0 + succ n` of our
goal with the right hand side. We do this with the `rewrite` spell. You can write
`rewrite [add_succ]`
(or even `rewrite [add_succ 0 n]` if you want to give Lean all the inputs instead of making it
figure them out itself).
"
Message (n : ) (ind_hyp: 0 + n = n) : succ (0 + n) = succ n =>
"Well-done! We're almost there. It's time to use our induction hypothesis.
Cast
`rewrite [ind_hyp]`
and finish by yourself.
"
Conclusion "Congratulations for completing your first inductive proof!"
Tactics rfl rewrite induction_on
Lemmas add_succ add_zero

@ -0,0 +1,21 @@
import NNG.GameServer.Commands
import NNG.MyNat
import NNG.TacticDocs
import NNG.LemmaDocs
Game "NNG"
Title "The Natural Number Game"
Introduction
"This is a sad day for mathematics. While trying to find glorious new foundations for mathematics,
someone removed the law of excluded middle and the axiom of choice. Unsurprisingly,
everything collapsed. A brave rescue team managed to retrieve our precious axioms from the wreckage
but now we need to rebuild all of mathematics from scratch.
As a beginning mathematics wizard, you've been tasked to rebuild the theory of natural numbers from
the axioms that Giuseppe Peano found under the collapsed tower of number theory. You've been equipped
with a level 1 spell book. Good luck."
Conclusion
"There is nothing else so far. Thanks for rescuing natural numbers!"

@ -0,0 +1,20 @@
axiom MyNat : Type
notation "" => MyNat
--axiom zero :
axiom succ :
@[instance] axiom MyOfNat (n : Nat) : OfNat n
@[instance] axiom myAddition : HAdd
@[instance] axiom myMultiplication : HMul
axiom add_zero : ∀ a : , a + 0 = a
axiom add_succ : ∀ a b : , a + succ b = succ (a + b)
@[elabAsElim] axiom myInduction {P : → Prop} (n : ) (h₀ : P 0) (h : ∀ n, P n → P (succ n)) : P n

@ -0,0 +1,7 @@
import NNG.Metadata
import NNG.Levels.Level1
import NNG.Levels.Level2
import NNG.Levels.Level3
import NNG.Levels.Level4
import NNG.Levels.Level5

@ -0,0 +1,148 @@
import NNG.GameServer.Commands
import NNG.Tactics
TacticDoc rfl
"
## Summary
`rfl` proves goals of the form `X = X`.
## Details
The `rfl` tactic will close any goal of the form `A = B`
where `A` and `B` are *exactly the same thing*.
### Example:
If it looks like this in the top right hand box:
```
Objects
a b c d :
Prove:
(a + b) * (c + d) = (a + b) * (c + d)
```
then
`rfl`
will close the goal and solve the level."
TacticDoc induction_on
"
## Summary
If `n : ` is in our objects list, then `induction_on n`
attempts to prove the current goal by induction on `n`, with the inductive
assumption in the `succ` case being `ind_hyp`.
### Example:
If your current goal is:
```
Objects
n :
Prove:
2 * n = n + n
```
then
`induction_on n`
will give us two goals:
```
Prove:
2 * 0 = 0 + 0
```
and
```
Objects
n : ,
Assumptions
ind_hyp : 2 * n = n + n
Prove:
2 * succ n = succ n + succ n
```
"
TacticDoc rewrite
"
## Summary
If `h` is a proof of `X = Y`, then `rewrite [h],` will change
all `X`s in the goal to `Y`s. Variants: `rewrite [<- h]` (changes
`Y` to `X`) and
`rewrite [h] at h2` (changes `X` to `Y` in hypothesis `h2` instead
of the goal).
## Details
The `rewrite` tactic is a way to do \"substituting in\". There
are two distinct situations where use this tactics.
1) If `h : A = B` is a hypothesis (i.e., a proof of `A = B`)
in your local context (the box in the top right)
and if your goal contains one or more `A`s, then `rewrite h`
will change them all to `B`'s.
2) The `rewrite` tactic will also work with proofs of theorems
which are equalities (look for them in the inventory).
For example, if your inventory contains `add_zero x : x + 0 = x`,
then `rewrite [add_zero]` will change `x + 0` into `x` in your goal
(or fail with an error if Lean cannot find `x + 0` in the goal).
Important note: if `h` is not a proof of the form `A = B`
or `A ↔ B` (for example if `h` is a function, an implication,
or perhaps even a proposition itself rather than its proof),
then `rewrite` is not the tactic you want to use. For example,
`rewrite [P = Q]` is never correct: `P = Q` is the true-false
statement itself, not the proof.
If `h : P = Q` is its proof, then `rewrite [h]` will work.
Pro tip 1: If `h : A = B` and you want to change
`B`s to `A`s instead, try `rewrite [<- h]` (get the arrow with `\\l` and
note that this is a small letter L, not a number 1).
### Example:
If it looks like this in the top right hand box:
```
Objects
x y :
Assumptions
h : x = y + y
Prove:
succ (x + 0) = succ (y + y)
```
then
`rewrite [add_zero]`
will change the goal into `succ x = succ (y + y)`, and then
`rewrite [h]`
will change the goal into `succ (y + y) = succ (y + y)`, which
can be solved with `rfl,`.
### Example:
You can use `rewrite` to change a hypothesis as well.
For example, if your local context looks like this:
```
Objects
x y :
Assumptions
h1 : x = y + 3
h2 : 2 * y = x
Prove:
y = 3
```
then `rewrite [h1] at h2` will turn `h2` into `h2 : 2 * y = y + 3`.
"
TacticDoc intro
"Useful to introduce stuff"
TacticSet basics := rfl induction_on intro rewrite

@ -0,0 +1,12 @@
import Lean
import NNG.MyNat
open Lean Elab Tactic
elab "swap" : tactic => do
match ← getGoals with
| g₁::g₂::t => setGoals (g₂::g₁::t)
| _ => pure ()
macro "induction_on" n:ident : tactic =>
`(tactic| refine myInduction $n ?base ?inductive_step; swap; clear $n; intro $n $(mkIdent `ind_hyp); swap)

@ -0,0 +1,81 @@
const WebSocket = require("ws");
const express = require("express");
const app = express()
const path = require("path")
const fs = require("fs");
const { spawn } = require('child_process');
const PORT = 8765;
const server = app
.use(express.static('./build'))
.listen(PORT, () => console.log(`Listening on ${PORT}`));
const wss = new WebSocket.Server({ server })
let cmd = "./build/bin/nng";
let cmdArgs = [];
// let cmd = "docker";
// let cmdArgs = ["run", "--runtime=runsc", "--network=none", "--rm", "-i", "nng4:latest"];
class ClientConnection {
content = Buffer.alloc(0)
constructor(ws){
console.log("Socket opened.")
this.ws = ws
this.ws.send("ok");
this.ws.on("message", (msg) => {
this.send(JSON.parse(msg.toString("utf8")));
})
this.ws.on("close", () => {
this.lean.kill();
console.log("Socket closed.")
})
this.lean = spawn(cmd, cmdArgs);
this.lean.stdout.on('readable', () => {
this.read();
});
this.lean.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
}
read () {
let chr;
while (chr = this.lean.stdout.read(1)) {
this.content = Buffer.concat([this.content,chr])
if (chr.toString() == "\n") {
console.log(this.content.toString())
this.ws.send(this.content.toString());
this.content = Buffer.alloc(0)
}
}
}
send(data) {
const str = JSON.stringify(data) + "\n";
const byteLength = Buffer.byteLength(str, "utf-8");
this.lean.stdin.cork();
this.lean.stdin.write(str);
this.lean.stdin.uncork();
}
}
wss.on("connection", function(ws) { // what should a websocket do on connection
new ClientConnection(ws)
})
// server.on('upgrade', async function upgrade(request, socket, head) {
// wss.handleUpgrade(request, socket, head, function done(ws) {
// wss.emit('connection', ws, request);
// });
// });

@ -0,0 +1,20 @@
import Lake
open Lake DSL
package nng {
-- add package configuration options here
}
lean_lib NNG {
-- add library configuration options here
}
lean_lib NNG.levels {
-- add library configuration options here
}
@[defaultTarget]
lean_exe nng {
root := `Main
supportInterpreter := true
}

@ -0,0 +1 @@
leanprover/lean4:nightly-2022-09-23

@ -0,0 +1,63 @@
const path = require("path");
const webpack = require("webpack");
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = env => {
const isDevelopment = env.NODE_ENV !== 'production';
const babelOptions = {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
isDevelopment && require.resolve('react-refresh/babel'),
].filter(Boolean),
};
global.$RefreshReg$ = () => {};
global.$RefreshSig$ = () => () => {};
return {
entry: "./client/src/index.js",
mode: isDevelopment ? 'development' : 'production',
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: [/server/, /node_modules/],
use: [{
loader: require.resolve('babel-loader'),
options: babelOptions,
}]
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/'
}
}
]
}
]
},
resolve: { extensions: ["*", ".js", ".jsx"] },
output: {
path: path.resolve(__dirname, "client/dist/"),
publicPath: "/client/dist/",
filename: "bundle.js"
},
devServer: {
contentBase: path.join(__dirname, "client/public/"),
port: 3000,
publicPath: "http://localhost:3000/dist/",
hotOnly: true
},
plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean),
};
}
Loading…
Cancel
Save