move everything
This commit is contained in:
parent
3ff7e76694
commit
9050581a10
25 changed files with 37 additions and 42 deletions
62
src/component/app/App.css
Normal file
62
src/component/app/App.css
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#root {
|
||||
display: grid;
|
||||
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
"content content content"
|
||||
"footer footer footer";
|
||||
|
||||
grid-template-columns: 100px 1fr 100px;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
/* grid-gap: 10px; */
|
||||
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
nav {
|
||||
grid-area: nav;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-area: content;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
aside {
|
||||
grid-area: side;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: right;
|
||||
grid-area: footer;
|
||||
|
||||
background-color: dodgerblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.command {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
padding: 0 6px 0 6px;
|
||||
margin: 0 4px 0 4px;
|
||||
}
|
||||
|
||||
.command.highlighted {
|
||||
background-color: dodgerblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.factoryReset {
|
||||
background-color: rgb(255, 0, 0);
|
||||
color: black;
|
||||
}
|
||||
181
src/component/app/App.tsx
Normal file
181
src/component/app/App.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import './App.css';
|
||||
import { ExprBlock, type ExprBlockState } from '../expr/ExprBlock';
|
||||
import { GlobalContext } from '../../context/GlobalContext';
|
||||
import { biggerExample, emptySet, factorial, higherOrder, higherOrder2Params, inc, initialEditorState, lambda2Params, nonEmptyEditorState, pushBool, tripleFunctionCallEditorState } from "./configurations";
|
||||
import { actionShortcuts } from './actions';
|
||||
// import { scoreResolved, type ResolvedType } from './eval';
|
||||
|
||||
|
||||
const examples: [string, ExprBlockState][] = [
|
||||
["empty editor" , initialEditorState ],
|
||||
["push to list" , nonEmptyEditorState ],
|
||||
["function w/ 4 params", tripleFunctionCallEditorState],
|
||||
["bigger example" , biggerExample ],
|
||||
["lambda 2 params" , lambda2Params ],
|
||||
["higher order" , higherOrder ],
|
||||
["higher order 2" , higherOrder2Params ],
|
||||
["push Bool" , pushBool ],
|
||||
["inc" , inc ],
|
||||
["empty set" , emptySet ],
|
||||
["factorial" , factorial ],
|
||||
];
|
||||
|
||||
type AppState = {
|
||||
history: ExprBlockState[],
|
||||
future: ExprBlockState[],
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
history: [initialEditorState],
|
||||
future: [],
|
||||
};
|
||||
|
||||
function loadFromLocalStorage(): AppState {
|
||||
if (localStorage["appState"]) {
|
||||
// try {
|
||||
const appState = JSON.parse(localStorage["appState"]); // may throw
|
||||
// if our state is corrupt, discover it eagerly:
|
||||
// evalEditorBlock(appState.history.at(-1), extendedEnv);
|
||||
|
||||
return appState; // all good
|
||||
// }
|
||||
// catch (e) {
|
||||
// console.log('error recovering state from localStorage (resetting):', e);
|
||||
// }
|
||||
}
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// load from localStorage
|
||||
const [appState, setAppState] = useState(loadFromLocalStorage());
|
||||
|
||||
const [syntacticSugar, setSyntacticSugar] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// persist accross reloads
|
||||
localStorage["appState"] = JSON.stringify(appState);
|
||||
}, [appState]);
|
||||
|
||||
const factoryReset = () => {
|
||||
setAppState(_ => defaultState);
|
||||
}
|
||||
|
||||
// factoryReset();
|
||||
|
||||
const pushHistory = (callback: (p: ExprBlockState) => ExprBlockState) => {
|
||||
setAppState(({history}) => {
|
||||
const newState = callback(history.at(-1)!);
|
||||
return {
|
||||
history: history.concat([newState]),
|
||||
future: [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onUndo = () => {
|
||||
setAppState(({history, future}) => ({
|
||||
history: history.slice(0,-1),
|
||||
future: future.concat(history.at(-1)!),
|
||||
}));
|
||||
};
|
||||
const onRedo = () => {
|
||||
setAppState(({history, future}) => ({
|
||||
history: history.concat(future.at(-1)!),
|
||||
future: future.slice(0,-1),
|
||||
}));
|
||||
};
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
if (e.key === "Z" && e.ctrlKey) {
|
||||
if (e.shiftKey) {
|
||||
if (appState.future.length > 0) {
|
||||
onRedo();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (appState.history.length > 1) {
|
||||
onUndo();
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window['APP_STATE'] = appState.history; // useful for debugging
|
||||
}, [appState.history]);
|
||||
|
||||
useEffect(() => {
|
||||
window.onkeydown = onKeyDown;
|
||||
}, []);
|
||||
|
||||
const [highlighted, setHighlighted] = useState(
|
||||
actionShortcuts.map(() => false));
|
||||
|
||||
const doHighlight = Object.fromEntries(actionShortcuts.map(([id], i) => {
|
||||
return [id, () => {
|
||||
setHighlighted(h => h.with(i, true));
|
||||
setTimeout(() => setHighlighted(h => h.with(i, false)), 100);
|
||||
}];
|
||||
}));
|
||||
|
||||
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
||||
// @ts-ignore
|
||||
if (e.target.value >= 0) {
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
pushHistory(_ => examples[e.target.value][1]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<button disabled={appState.history.length===1} onClick={onUndo}>Undo ({appState.history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
|
||||
<button disabled={appState.future.length===0} onClick={onRedo}>Redo ({appState.future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></button>
|
||||
{
|
||||
actionShortcuts.map(([_, keys, descr], i) =>
|
||||
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
|
||||
{keys.map((key, j) => <kbd key={j}>{key}</kbd>)}
|
||||
|
||||
{descr}
|
||||
</span>)
|
||||
}
|
||||
<select onChange={onSelectExample} value={-1}>
|
||||
<option value={-1}>load example...</option>
|
||||
{
|
||||
examples.map(([name], i) => {
|
||||
return <option key={i} value={i}>{name}</option>;
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<button className="factoryReset" onClick={factoryReset}>
|
||||
FACTORY RESET
|
||||
</button>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
checked={syntacticSugar}
|
||||
onChange={e => setSyntacticSugar(e.target.checked)}/>
|
||||
syntactic sugar
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<main onKeyDown={onKeyDown}>
|
||||
<GlobalContext value={{undo: onUndo, redo: onRedo, doHighlight, syntacticSugar}}>
|
||||
<ExprBlock
|
||||
state={appState.history.at(-1)!}
|
||||
setState={pushHistory}
|
||||
onCancel={() => {}}
|
||||
score={() => 0}
|
||||
/>
|
||||
</GlobalContext>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<a href="https://deemz.org/git/joeri/dope2-webapp">Source code</a>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
62
src/component/app/actions.ts
Normal file
62
src/component/app/actions.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { initialEditorState } from "./configurations";
|
||||
// import { removeFocus } from "./eval";
|
||||
|
||||
const removeFocus = state => state;
|
||||
|
||||
export const actionShortcuts: [string, string[], string][] = [
|
||||
["call" , ['c'], "expr ⌴" ],
|
||||
["transform", ['t'], "⌴ expr" ],
|
||||
["assign" , ['a'], "let (⌴ = expr) in ⌴"],
|
||||
["declare" , ['d'], "let (⌴ = ⌴) in expr"],
|
||||
["lambda" , ['l'], "λ⌴. expr" ],
|
||||
];
|
||||
|
||||
export function getActions(globalContext, setState) {
|
||||
return {
|
||||
c: () => {
|
||||
setState(state => ({
|
||||
kind: "call",
|
||||
fn: removeFocus(state),
|
||||
input: initialEditorState,
|
||||
}));
|
||||
globalContext?.doHighlight.call();
|
||||
},
|
||||
t: () => {
|
||||
setState(state => ({
|
||||
kind: "call",
|
||||
fn: initialEditorState,
|
||||
input: removeFocus(state),
|
||||
}));
|
||||
globalContext?.doHighlight.transform();
|
||||
},
|
||||
a: () => {
|
||||
setState(state => ({
|
||||
kind: "let",
|
||||
name: "",
|
||||
focus: true,
|
||||
value: removeFocus(state),
|
||||
inner: removeFocus(initialEditorState),
|
||||
}));
|
||||
globalContext?.doHighlight.assign();
|
||||
},
|
||||
d: () => {
|
||||
setState(state => ({
|
||||
kind: "let",
|
||||
name: "",
|
||||
focus: true,
|
||||
value: removeFocus(initialEditorState),
|
||||
inner: removeFocus(state),
|
||||
}));
|
||||
globalContext?.doHighlight.declare();
|
||||
},
|
||||
l: () => {
|
||||
setState(state => ({
|
||||
kind: "lambda",
|
||||
paramName: "",
|
||||
focus: true,
|
||||
expr: removeFocus(state),
|
||||
}));
|
||||
globalContext?.doHighlight.lambda();
|
||||
},
|
||||
};
|
||||
}
|
||||
144
src/component/app/configurations.ts
Normal file
144
src/component/app/configurations.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import type { ExprBlockState } from "../expr/ExprBlock";
|
||||
|
||||
export const initialEditorState: ExprBlockState = {
|
||||
kind: "input",
|
||||
text: "",
|
||||
value: { kind: "text" },
|
||||
focus: true,
|
||||
};
|
||||
|
||||
export const nonEmptyEditorState: ExprBlockState = {
|
||||
kind: "call",
|
||||
fn: {
|
||||
kind: "call",
|
||||
fn: {
|
||||
kind: "input",
|
||||
text: "list.push",
|
||||
value: { kind: "name" },
|
||||
focus: false,
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
text: "list.emptyList",
|
||||
value: { kind: "name" },
|
||||
focus: false,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
text: "42",
|
||||
value: { kind: "literal", type: "Int" },
|
||||
focus: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const tripleFunctionCallEditorState: ExprBlockState = {
|
||||
kind: "call",
|
||||
fn: {
|
||||
kind: "call",
|
||||
fn: {
|
||||
kind: "call",
|
||||
fn: {
|
||||
kind: "call",
|
||||
fn: {
|
||||
kind: "input",
|
||||
text: "functionWith4Params",
|
||||
value: { kind: "name" },
|
||||
focus: false,
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
text: "42",
|
||||
value: { kind: "literal", type: "Int" },
|
||||
focus: false,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
text: "43",
|
||||
value: { kind: "literal", type: "Int" },
|
||||
focus: false,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
text: "44",
|
||||
value: { kind: "literal", type: "Int" },
|
||||
focus: false,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
text: "45",
|
||||
value: { kind: "literal", type: "Int" },
|
||||
focus: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const biggerExample: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"input","text":"","value":{"kind":"text"},"focus":false},"name":"myListInc","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"myList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"inc","value":{"kind":"name"},"focus":false}}},"input":{"kind":"input","text":"id","value":{"kind":"name"},"focus":true}}},"name":"myList","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"list.emptyList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"2","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"3","value":{"kind":"literal","type":"Int"},"focus":false}}},"name":"id","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}}},"name":"inc","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":true}}}};
|
||||
|
||||
export const lambda2Params: ExprBlockState = {
|
||||
"kind": "let",
|
||||
"inner": {
|
||||
"kind": "input",
|
||||
"text": "",
|
||||
"value": {
|
||||
"kind": "text"
|
||||
},
|
||||
"focus": false
|
||||
},
|
||||
"name": "myAddInt",
|
||||
"focus": false,
|
||||
"value": {
|
||||
"kind": "lambda",
|
||||
"paramName": "x",
|
||||
"focus": false,
|
||||
"expr": {
|
||||
"kind": "lambda",
|
||||
"paramName": "y",
|
||||
"focus": false,
|
||||
"expr": {
|
||||
"kind": "call",
|
||||
"fn": {
|
||||
"kind": "call",
|
||||
"fn": {
|
||||
"kind": "input",
|
||||
"text": "addInt",
|
||||
"value": {
|
||||
"kind": "name"
|
||||
},
|
||||
"focus": false
|
||||
},
|
||||
"input": {
|
||||
"kind": "input",
|
||||
"text": "x",
|
||||
"value": {
|
||||
"kind": "name"
|
||||
},
|
||||
"focus": false
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"kind": "input",
|
||||
"text": "y",
|
||||
"value": {
|
||||
"kind": "name"
|
||||
},
|
||||
"focus": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const higherOrder: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"input","text":"","value":{"kind":"text"},"focus":false},"name":"myBinaryApply","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"lambda","focus":false,"paramName":"fn","expr":{"kind":"call","fn":{"kind":"input","text":"fn","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}}}}};
|
||||
|
||||
export const higherOrder2Params: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"call","fn":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"myBinaryApply","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}},"input":{"kind":"input","text":"2","value":{"kind":"literal","type":"Int"},"focus":false}},"input":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":true}},"name":"myBinaryApply","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"lambda","focus":false,"paramName":"y","expr":{"kind":"lambda","focus":false,"paramName":"fn","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"fn","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"y","value":{"kind":"name"},"focus":true}}}}}};
|
||||
|
||||
export const pushBool: ExprBlockState = {"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"list.emptyList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"Bool","value":{"kind":"name"},"focus":true}};
|
||||
|
||||
export const inc: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"input","text":"","value":{"kind":"name"},"focus":false},"name":"inc","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":true}}}};
|
||||
|
||||
export const emptySet: ExprBlockState = {"kind":"call","fn":{"kind":"input","text":"set.emptySet","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"","value":{"kind":"text"},"focus":true}};
|
||||
|
||||
export const factorial: ExprBlockState = {"kind":"lambda","paramName":"factorial","focus":true,"expr":{"kind":"lambda","paramName":"n","focus":true,"expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"leqZero","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"n","value":{"kind":"name"},"focus":false}},"input":{"kind":"lambda","paramName":"_","focus":false,"expr":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"lambda","paramName":"_","focus":false,"expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"mulInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"n","value":{"kind":"name"},"focus":true}},"input":{"kind":"call","fn":{"kind":"input","text":"factorial","value":{"kind":"name"},"focus":true},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"n","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"-1","value":{"kind":"literal","type":"Int"},"focus":false}}}}}}}};
|
||||
112
src/component/expr/CallBlock.css
Normal file
112
src/component/expr/CallBlock.css
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
.functionBlock {
|
||||
border: solid 1px darkgray;
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.functionName {
|
||||
/* text-align: center; */
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.inputParam:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
clip-path: polygon(1% 0%, 100% 50%, 0% 100%);
|
||||
height: 100%;
|
||||
width: var(--param-arrow-width);
|
||||
right: calc(var(--param-arrow-width)*(-1) + .2px);
|
||||
}
|
||||
.inputParam {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
position: relative; /* to ensure the :after (which is absolute) is relative to ourselves */
|
||||
flex-grow: 1;
|
||||
--param-arrow-width: 8px;
|
||||
margin-right: calc(var(--param-arrow-width)*2);
|
||||
}
|
||||
|
||||
/* Count nested level AFTER .outputParam (resets the depth) */
|
||||
.outputParam > .inputParam:after {
|
||||
background-color: rgb(242, 253, 146);
|
||||
}
|
||||
.outputParam > .inputParam {
|
||||
background-color: rgb(242, 253, 146);
|
||||
}
|
||||
.outputParam > .inputParam > .inputParam {
|
||||
background-color: rgb(180, 248, 214);
|
||||
}
|
||||
.outputParam > .inputParam > .inputParam:after {
|
||||
background-color: rgb(180, 248, 214);
|
||||
}
|
||||
.outputParam > .inputParam > .inputParam > .inputParam {
|
||||
background-color: rgb(153, 212, 214);
|
||||
}
|
||||
.outputParam > .inputParam > .inputParam > .inputParam:after {
|
||||
background-color: rgb(153, 212, 214);
|
||||
}
|
||||
.outputParam > .inputParam > .inputParam > .inputParam > .inputParam {
|
||||
background-color: rgb(111, 186, 209);
|
||||
}
|
||||
.outputParam > .inputParam > .inputParam > .inputParam > .inputParam:after {
|
||||
background-color: rgb(111, 186, 209);
|
||||
}
|
||||
|
||||
.typeAnnot {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.outputParam {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 0px;
|
||||
display: inline-block;
|
||||
background-color: rgb(233, 224, 205);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.functionBlock.unifyError > .functionParams > .outputParam {
|
||||
background-color: pink;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam {
|
||||
background-color: pink;
|
||||
color: black;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam:after {
|
||||
background-color: pink;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam {
|
||||
background-color: pink;
|
||||
color: black;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after {
|
||||
background-color: pink;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam {
|
||||
background-color: pink;
|
||||
color: black;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam:after {
|
||||
background-color: pink;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam {
|
||||
background-color: pink;
|
||||
color: black;
|
||||
}
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam:after {
|
||||
background-color: pink;
|
||||
}
|
||||
|
||||
.inputParam.offending {
|
||||
background-color: darkred !important;
|
||||
color: white !important;
|
||||
}
|
||||
.inputParam.offending:after {
|
||||
background-color: darkred !important;
|
||||
}
|
||||
135
src/component/expr/CallBlock.tsx
Normal file
135
src/component/expr/CallBlock.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { EnvContext } from "../../context/EnvContext";
|
||||
// import { addFocusRightMost, evalCallBlock2, evalExprBlock, recomputeTypeVarsForEnv, scoreResolved, type Environment, type ResolvedType } from "./eval";
|
||||
import { ExprBlock, type ExprBlockState, type SetStateFn, type State2Props } from "./ExprBlock";
|
||||
import { GlobalContext } from "../../context/GlobalContext";
|
||||
|
||||
import { getActions } from "../app/actions";
|
||||
import "./CallBlock.css";
|
||||
import { CallContext } from "../../context/CallContext";
|
||||
import { inferTypeCall, type Environment } from "../../eval/infer_type";
|
||||
import { Type } from "../other/Type";
|
||||
|
||||
export interface CallBlockState {
|
||||
kind: "call";
|
||||
fn: ExprBlockState;
|
||||
input: ExprBlockState;
|
||||
}
|
||||
|
||||
export interface CallBlockProps<
|
||||
FnState=ExprBlockState,
|
||||
InputState=ExprBlockState,
|
||||
> extends State2Props<CallBlockState,ExprBlockState> {}
|
||||
|
||||
function nestedFnProperties({state, setState, score}: CallBlockProps, env: Environment) {
|
||||
const setFn = (callback: SetStateFn) => {
|
||||
setState(state => ({...state, fn: callback(state.fn)}));
|
||||
};
|
||||
const onFnCancel = () => {
|
||||
setState(state => state.input); // we become our input
|
||||
};
|
||||
const scoreFn = (fnSuggestion: ExprBlockState) => {
|
||||
return score({
|
||||
kind: "call",
|
||||
fn: fnSuggestion,
|
||||
input: state.input,
|
||||
});
|
||||
};
|
||||
return {state: state.fn, setState: setFn, onCancel: onFnCancel, score: scoreFn};
|
||||
}
|
||||
|
||||
function nestedInputProperties({state, setState, score}: CallBlockProps, env: Environment) {
|
||||
const setInput = (callback: SetStateFn) => {
|
||||
setState(state => ({...state, input: callback(state.input)}));
|
||||
};
|
||||
const onInputCancel = () => {
|
||||
setState(state => /*addFocusRightMost*/(state.fn)); // we become our function
|
||||
};
|
||||
const scoreInput = (inputSuggestion: ExprBlockState) => {
|
||||
return score({
|
||||
kind: "call",
|
||||
fn: state.fn,
|
||||
input: inputSuggestion,
|
||||
});
|
||||
};
|
||||
return {state: state.input, setState: setInput, onCancel: onInputCancel, score: scoreInput};
|
||||
}
|
||||
|
||||
// function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: ResolvedType) => number, env) {
|
||||
// // dirty, but works:
|
||||
// const [fnR, env2] = recomputeTypeVarsForEnv('<fn>', fn, env);
|
||||
// const [inR, env3] = recomputeTypeVarsForEnv('<in>', input, env2);
|
||||
// const [resolved] = evalCallBlock2(fnR, inR, env3);
|
||||
// const score = scoreResolved(resolved, outPriority);
|
||||
// return score;
|
||||
// }
|
||||
|
||||
export function CallBlock(props: CallBlockProps) {
|
||||
const env = useContext(EnvContext);
|
||||
const globalContext = useContext(GlobalContext);
|
||||
const addParam = getActions(globalContext, props.setState).c;
|
||||
// const [resolved] = evalExprBlock(props.state, env);
|
||||
// return <span className={"functionBlock" + ((resolved.kind === "error") ? " unifyError" : "")}>
|
||||
const typeInfo = inferTypeCall(props.state, env);
|
||||
return <span className={"functionBlock"}>
|
||||
<CallContext value={{addParam}}>
|
||||
<FunctionHeader {...props} addParam={addParam} />
|
||||
<div className="functionParams">
|
||||
<div className="outputParam">
|
||||
{/* Sequence of input parameters */}
|
||||
<InputParams
|
||||
{...props}
|
||||
depth={0}
|
||||
// errorDepth={(resolved.kind === "error") ? (resolved.depth) : -1}
|
||||
errorDepth={-1}
|
||||
addParam={addParam}
|
||||
/>
|
||||
{/* { (resolved.kind === "error") && resolved.e.toString()
|
||||
|| (resolved.kind === "value") && <Value dynamic={resolved} />
|
||||
|| "unknown" } */}
|
||||
:: <Type type={typeInfo.type} />
|
||||
</div>
|
||||
</div>
|
||||
</CallContext>
|
||||
</span>;
|
||||
}
|
||||
|
||||
function FunctionHeader(props) {
|
||||
const env = useContext(EnvContext);
|
||||
const globalContext = useContext(GlobalContext);
|
||||
const nestedProperties = nestedFnProperties(props, env);
|
||||
if (props.state.fn.kind === "call" && globalContext?.syntacticSugar) {
|
||||
// if the function we're calling is itself the result of a function call,
|
||||
// then we are anonymous, and so we don't draw a function name
|
||||
return <FunctionHeader {...nestedProperties} />;
|
||||
}
|
||||
else {
|
||||
// end of recursion - draw function name
|
||||
return <span className="functionName">
|
||||
𝑓𝑛
|
||||
<ExprBlock {...nestedProperties} />
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
||||
function InputParams({ depth, errorDepth, ...rest }) {
|
||||
const env = useContext(EnvContext);
|
||||
const globalContext = useContext(GlobalContext);
|
||||
const isOffending = depth === errorDepth;
|
||||
return <div className={"inputParam" + (isOffending ? " offending" : "")}>
|
||||
{rest.state.fn.kind === "call"
|
||||
&& globalContext?.syntacticSugar
|
||||
&& <InputParams
|
||||
{...nestedFnProperties(rest as CallBlockProps, env)}
|
||||
depth={depth+1}
|
||||
errorDepth={errorDepth}
|
||||
/>}
|
||||
{/* Our own input param */}
|
||||
<EnvContext value={inferTypeCall(rest.state, env).inputEnv}>
|
||||
<ExprBlock
|
||||
{...nestedInputProperties(rest as CallBlockProps, env)}
|
||||
/>
|
||||
</EnvContext>
|
||||
</div>;
|
||||
}
|
||||
56
src/component/expr/ExprBlock.css
Normal file
56
src/component/expr/ExprBlock.css
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
.editor {
|
||||
padding: 2px;;
|
||||
}
|
||||
.editor.error {
|
||||
border: 1px solid red;
|
||||
display: inline-block;
|
||||
}
|
||||
.editor.unknown {
|
||||
border: 1px dashed dodgerblue;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
*:hover:not(:has(> *:hover)) {
|
||||
/* useful for debugging: */
|
||||
/* border-width: 2px !important; */
|
||||
}
|
||||
|
||||
.offending .error {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.typeSignature {
|
||||
display: inline-block;
|
||||
/* z-index: 1; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.typeSignature.gotDebug {
|
||||
background-color: gold;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.typeDebug {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.typeSignature:hover > .typeDebug {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
white-space-collapse: preserve;
|
||||
width: max-content;
|
||||
background-color: #d2ebf1e0;
|
||||
color: black;
|
||||
font-family: var(--my-monospace-font);
|
||||
padding: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.editor:hover > .typeSignature {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
72
src/component/expr/ExprBlock.tsx
Normal file
72
src/component/expr/ExprBlock.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { getType } from "dope2";
|
||||
|
||||
import { CallBlock, type CallBlockProps, type CallBlockState } from "./CallBlock";
|
||||
import { EnvContext } from "../../context/EnvContext";
|
||||
import { GlobalContext } from "../../context/GlobalContext";
|
||||
import { InputBlock, type InputBlockProps, type InputBlockState } from "./InputBlock";
|
||||
import { LambdaBlock, type LambdaBlockProps, type LambdaBlockState } from "./LambdaBlock";
|
||||
import { LetInBlock, type LetInBlockProps, type LetInBlockState } from "./LetInBlock";
|
||||
// import { evalExprBlock, type ResolvedType } from "./eval";
|
||||
|
||||
import "./ExprBlock.css";
|
||||
import { Input } from "../other/Input";
|
||||
import { getActions } from "../app/actions";
|
||||
|
||||
export type ExprBlockState =
|
||||
InputBlockState
|
||||
| CallBlockState
|
||||
| LetInBlockState
|
||||
| LambdaBlockState;
|
||||
|
||||
export type SetStateFn<InType = ExprBlockState, OutType = InType> = (state: InType) => OutType;
|
||||
|
||||
export interface State2Props<InType, OutType = InType> {
|
||||
state: InType;
|
||||
setState: (callback: SetStateFn<InType, OutType>) => void;
|
||||
score: (suggestion: ExprBlockState) => number;
|
||||
}
|
||||
|
||||
interface ExprBlockProps extends State2Props<ExprBlockState> {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ExprBlock(props: ExprBlockProps) {
|
||||
const env = useContext(EnvContext);
|
||||
const globalContext = useContext(GlobalContext);
|
||||
|
||||
const renderBlock = {
|
||||
input: () => <InputBlock {...props as InputBlockProps} />,
|
||||
call: () => <CallBlock {...props as CallBlockProps} />,
|
||||
let: () => <LetInBlock {...props as LetInBlockProps} />,
|
||||
lambda: () => <LambdaBlock {...props as LambdaBlockProps} />,
|
||||
};
|
||||
|
||||
// const [resolved] = evalExprBlock(props.state, env);
|
||||
// const typeInfo = inferType(props.state, env);
|
||||
const actions = getActions(globalContext, props.setState);
|
||||
const extraHandlers = Object.fromEntries(Object.entries(actions).map(([shortcut, action]) =>
|
||||
[shortcut, (e) => { e.preventDefault(); action(); }]))
|
||||
|
||||
// return <span className={"editor" + ((resolved.kind!=="value") ? " "+resolved.kind : "")}>
|
||||
|
||||
return <span className={"editor"}>
|
||||
{renderBlock[props.state.kind]()}
|
||||
{/* @ts-ignore */}
|
||||
{/* <div className={"typeSignature" + (resolved.__debug ? ' gotDebug' : '')}> */}
|
||||
{/* :: <Type type={typeInfo.type} /> */}
|
||||
{/* @ts-ignore */}
|
||||
{/* {resolved.__debug && <div className="typeDebug">{resolved.__debug}</div>} */}
|
||||
{/* </div> */}
|
||||
<Input
|
||||
placeholder="<c>"
|
||||
text=""
|
||||
suggestion=""
|
||||
onEnter={() => {}}
|
||||
onCancel={props.onCancel}
|
||||
onTextChange={() => {}}
|
||||
extraHandlers={extraHandlers}
|
||||
/>
|
||||
</span>;
|
||||
}
|
||||
47
src/component/expr/InputBlock.css
Normal file
47
src/component/expr/InputBlock.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
.inputBlock {
|
||||
position: relative;
|
||||
}
|
||||
.editable {
|
||||
position: relative;
|
||||
border: 0;
|
||||
font-size: 10pt;
|
||||
font-family: var(--my-monospace-font);
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
cursor: text;
|
||||
outline: 0;
|
||||
}
|
||||
.suggest {
|
||||
top: 2.4px;
|
||||
position: absolute;
|
||||
color: #aaa;
|
||||
}
|
||||
.suggestionsPlaceholder {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.suggestions {
|
||||
display: block;
|
||||
color: black;
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
border: solid 1px dodgerblue;
|
||||
cursor: pointer;
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow: auto;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
width: max-content;
|
||||
max-width: 500px;
|
||||
}
|
||||
.selected {
|
||||
background-color: dodgerblue;
|
||||
color: white;
|
||||
}
|
||||
.border-around-input {
|
||||
border: 1px solid black;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
199
src/component/expr/InputBlock.tsx
Normal file
199
src/component/expr/InputBlock.tsx
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { memo, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { trie } from "dope2";
|
||||
|
||||
import { EnvContext } from "../../context/EnvContext";
|
||||
// import type { Environment, ResolvedType } from "./eval";
|
||||
import "./InputBlock.css";
|
||||
import { Type } from "../other/Type";
|
||||
import type { ExprBlockState, State2Props } from "./ExprBlock";
|
||||
// import { attemptParseLiteral } from "./eval";
|
||||
import { Input } from "../other/Input";
|
||||
import { CallContext } from "../../context/CallContext";
|
||||
import { getActions } from "../app/actions";
|
||||
import { GlobalContext } from "../../context/GlobalContext";
|
||||
import { inferTypeInput } from "../../eval/infer_type";
|
||||
|
||||
interface Literal {
|
||||
kind: "literal";
|
||||
type: string; // todo: store (and serialize) real type
|
||||
};
|
||||
interface Name {
|
||||
kind: "name";
|
||||
}
|
||||
interface Text {
|
||||
kind: "text";
|
||||
}
|
||||
export type InputValueType = Literal | Name | Text;
|
||||
|
||||
export interface InputBlockState {
|
||||
kind: "input";
|
||||
text: string;
|
||||
value: InputValueType;
|
||||
focus: boolean
|
||||
}
|
||||
|
||||
// export type SuggestionType = ["literal"|"name", string, ResolvedType];
|
||||
// export type PrioritizedSuggestionType = [number, ...SuggestionType];
|
||||
|
||||
export interface InputBlockProps extends State2Props<InputBlockState,ExprBlockState> {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
// const computeSuggestions = (
|
||||
// text: string,
|
||||
// env: Environment,
|
||||
// score: InputBlockProps['score'],
|
||||
// ): PrioritizedSuggestionType[] => {
|
||||
// const literals = attemptParseLiteral(text, env);
|
||||
// const ls: SuggestionType[] = [
|
||||
// // literals
|
||||
// ... literals.map((resolved) => ["literal", text, resolved]),
|
||||
|
||||
// // names
|
||||
// ... trie.suggest(env.names)(text)(Infinity)
|
||||
// .map(([name, resolved]) => ["name", name, resolved]),
|
||||
// ]
|
||||
// // return []; // <-- uncomment to disable suggestions (useful for debugging)
|
||||
// return ls
|
||||
// .map((suggestion: SuggestionType) =>
|
||||
// [score(suggestion[2]), ...suggestion] as PrioritizedSuggestionType)
|
||||
// .sort(([priorityA], [priorityB]) => priorityB - priorityA)
|
||||
// }
|
||||
|
||||
export function InputBlock({ state, setState, /*score,*/ onCancel }: InputBlockProps) {
|
||||
const {text, focus} = state;
|
||||
const globalContext = useContext(GlobalContext);
|
||||
const env = useContext(EnvContext);
|
||||
const callContext = useContext(CallContext);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [i, setI] = useState(0); // selected suggestion idx
|
||||
|
||||
const singleSuggestion = trie.growPrefix(env.names)(text);
|
||||
// const suggestions = useMemo(() => computeSuggestions(text, env, score), [text, score, env]);
|
||||
const suggestions = useMemo(() => [], []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (focus) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [focus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (suggestions.length >= i) {
|
||||
setI(0);
|
||||
}
|
||||
}, [suggestions.length]);
|
||||
|
||||
const onTextChange = newText => {
|
||||
setState(state => ({...state,
|
||||
text: newText,
|
||||
value: (trie.get(env.names)(newText) ? {
|
||||
kind: "name",
|
||||
} : state.value),
|
||||
}));
|
||||
}
|
||||
|
||||
const onSelectSuggestion = () => {
|
||||
// const [_priority, kind, name, dynamic] = suggestions[i];
|
||||
// if (kind === "literal") {
|
||||
// setState(state => ({
|
||||
// ...state,
|
||||
// text: name,
|
||||
// value: {kind, type: prettyT(getType(dynamic))},
|
||||
// }));
|
||||
// }
|
||||
// else {
|
||||
// setState(state => ({
|
||||
// ...state,
|
||||
// text: name,
|
||||
// value: {kind},
|
||||
// }))
|
||||
// }
|
||||
};
|
||||
|
||||
const extraHandlers = {
|
||||
ArrowDown: (e) => {
|
||||
setI((i + 1) % suggestions.length);
|
||||
e.preventDefault();
|
||||
},
|
||||
ArrowUp: (e) => {
|
||||
setI((suggestions.length + i - 1) % suggestions.length);
|
||||
e.preventDefault();
|
||||
},
|
||||
" ": (e) => {
|
||||
if (text.length > 0) {
|
||||
if (callContext.addParam) {
|
||||
callContext.addParam();
|
||||
}
|
||||
else {
|
||||
const actions = getActions(globalContext, setState);
|
||||
actions.c();
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
},
|
||||
};
|
||||
|
||||
const typeInfo = inferTypeInput(state, env);
|
||||
|
||||
return <><Input
|
||||
placeholder="<name or literal>"
|
||||
onCancel={onCancel}
|
||||
onEnter={onSelectSuggestion}
|
||||
onTextChange={onTextChange}
|
||||
text={text}
|
||||
suggestion={singleSuggestion}
|
||||
extraHandlers={extraHandlers}
|
||||
>
|
||||
<span className="suggestionsPlaceholder">
|
||||
<Suggestions
|
||||
suggestions={suggestions}
|
||||
onSelect={onSelectSuggestion}
|
||||
i={i} setI={setI} />
|
||||
</span>
|
||||
</Input>
|
||||
::<Type type={typeInfo.type} />
|
||||
</>
|
||||
}
|
||||
|
||||
function Suggestions({ suggestions, onSelect, i, setI }) {
|
||||
return <>{(suggestions.length > 0) &&
|
||||
<div className={"suggestions"}>
|
||||
{suggestions.map((suggestion, j) =>
|
||||
<SuggestionMemo key={j}
|
||||
{...{setI, j,
|
||||
onSelect,
|
||||
highlighted: i===j,
|
||||
suggestion}}/>)}
|
||||
</div>
|
||||
}</>;
|
||||
}
|
||||
|
||||
interface SuggestionProps {
|
||||
setI: any;
|
||||
j: number;
|
||||
onSelect: any;
|
||||
highlighted: boolean;
|
||||
// suggestion: PrioritizedSuggestionType;
|
||||
}
|
||||
|
||||
function Suggestion({ setI, j, onSelect, highlighted, /*suggestion: [priority, kind, text, resolved]*/ }: SuggestionProps) {
|
||||
const onMouseEnter = j => () => {
|
||||
setI(j);
|
||||
};
|
||||
const onMouseDown = j => () => {
|
||||
setI(j);
|
||||
onSelect();
|
||||
};
|
||||
return <div
|
||||
key={`${j}_${name}`}
|
||||
className={(highlighted ? " selected" : "")}
|
||||
onMouseEnter={onMouseEnter(j)}
|
||||
onMouseDown={onMouseDown(j)}>
|
||||
{/* ({priority}) ({kind}) {text} :: <Type type={resolved.t} /> */}
|
||||
</div>
|
||||
}
|
||||
|
||||
const SuggestionMemo = memo<SuggestionProps>(Suggestion);
|
||||
9
src/component/expr/LambdaBlock.css
Normal file
9
src/component/expr/LambdaBlock.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.lambdaBlock {
|
||||
display: inline-block;
|
||||
border: solid 1px darkgrey;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.lambdaInner {
|
||||
display: inline-block;
|
||||
}
|
||||
82
src/component/expr/LambdaBlock.tsx
Normal file
82
src/component/expr/LambdaBlock.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
// import { eqType, getSymbol, reduceUnification } from "dope2";
|
||||
|
||||
import { ExprBlock, type ExprBlockState, type State2Props } from "./ExprBlock";
|
||||
import { EnvContext } from "../../context/EnvContext";
|
||||
// import { evalExprBlock, evalLambdaBlock, makeInnerEnv, makeTypeVar } from "./eval";
|
||||
|
||||
import "./LambdaBlock.css";
|
||||
import { Type } from "../other/Type";
|
||||
import { Input } from "../other/Input";
|
||||
import { inferTypeLambda } from "../../eval/infer_type";
|
||||
|
||||
export interface LambdaBlockState {
|
||||
kind: "lambda";
|
||||
paramName: string;
|
||||
focus: boolean;
|
||||
expr: ExprBlockState;
|
||||
}
|
||||
|
||||
export interface LambdaBlockProps<
|
||||
FnState=ExprBlockState,
|
||||
InputState=ExprBlockState,
|
||||
> extends State2Props<LambdaBlockState,ExprBlockState> {
|
||||
}
|
||||
|
||||
|
||||
export function LambdaBlock({state, setState, score}: LambdaBlockProps) {
|
||||
const env = useContext(EnvContext);
|
||||
|
||||
const setParamName = paramName => setState(state => ({
|
||||
...state,
|
||||
paramName,
|
||||
}));
|
||||
const setExpr = callback => setState(state => ({
|
||||
...state,
|
||||
expr: callback(state.expr),
|
||||
}));
|
||||
|
||||
const {paramType, innerEnv} = inferTypeLambda(state, env);
|
||||
|
||||
// const [lambdaResolved, _, innerEnv] = evalLambdaBlock(state.paramName, state.expr, env);
|
||||
|
||||
// const inferredParamType = lambdaResolved.t.params[0](lambdaResolved.t);
|
||||
|
||||
// const innerEnv = env; // todo: change this
|
||||
|
||||
return <span className="lambdaBlock">
|
||||
<span className="keyword">λ</span>
|
||||
|
||||
<span className="lambdaInputParam">
|
||||
<Input
|
||||
placeholder="<name>"
|
||||
text={state.paramName}
|
||||
suggestion=""
|
||||
onEnter={() => {}}
|
||||
onCancel={() => {}}
|
||||
onTextChange={txt => setParamName(txt)}
|
||||
extraHandlers={{}}
|
||||
/>
|
||||
</span>
|
||||
<div className="typeSignature">
|
||||
:: <Type type={paramType} />
|
||||
</div>
|
||||
|
||||
<span className="keyword">:</span>
|
||||
|
||||
<div className="lambdaInner">
|
||||
<EnvContext value={innerEnv}>
|
||||
<ExprBlock
|
||||
state={state.expr}
|
||||
setState={setExpr}
|
||||
onCancel={() => setState(state => state.expr)}
|
||||
score={(s) => {
|
||||
// console.log('suggestionPriority of lambdaInner... just passing through');
|
||||
return score(s);
|
||||
}}
|
||||
/>
|
||||
</EnvContext>
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
23
src/component/expr/LetInBlock.css
Normal file
23
src/component/expr/LetInBlock.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.letIn {
|
||||
display: inline-block;
|
||||
/* border: 1px solid darkgrey; */
|
||||
}
|
||||
|
||||
.inner {
|
||||
border: 1px solid darkgrey;
|
||||
margin-left: 4px
|
||||
}
|
||||
|
||||
.decl {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.column.rightAlign {
|
||||
/* text-align: right; */
|
||||
}
|
||||
|
||||
.column {
|
||||
vertical-align: top;
|
||||
}
|
||||
117
src/component/expr/LetInBlock.tsx
Normal file
117
src/component/expr/LetInBlock.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { ExprBlock, type ExprBlockState } from "./ExprBlock";
|
||||
import { EnvContext } from "../../context/EnvContext";
|
||||
// import { evalExprBlock, makeInnerEnv, scoreResolved, type ResolvedType } from "./eval";
|
||||
import { type State2Props } from "./ExprBlock";
|
||||
import { GlobalContext } from "../../context/GlobalContext";
|
||||
|
||||
import "./LetInBlock.css";
|
||||
import { Input } from "../other/Input";
|
||||
import { inferTypeLet } from "../../eval/infer_type";
|
||||
import { Type } from "../other/Type";
|
||||
|
||||
export interface LetInBlockState {
|
||||
kind: "let";
|
||||
name: string;
|
||||
focus: boolean;
|
||||
value: ExprBlockState;
|
||||
inner: ExprBlockState;
|
||||
}
|
||||
|
||||
export interface LetInBlockProps extends State2Props<LetInBlockState,ExprBlockState> {
|
||||
}
|
||||
|
||||
export function LetInBlock(props: LetInBlockProps) {
|
||||
return <span className="letIn">
|
||||
<div className="decl">
|
||||
<DeclColumns {...props} />
|
||||
</div>
|
||||
<div className="inner">
|
||||
<InnerMost {...props} />
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
|
||||
function DeclColumns({state, setState, score}) {
|
||||
const env = useContext(EnvContext);
|
||||
const globalContext = useContext(GlobalContext);
|
||||
|
||||
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
|
||||
const setValue = callback => setState(state => ({...state, value: callback(state.value)}));
|
||||
|
||||
// const valueSuggestionPriority = (suggestion: ResolvedType) => {
|
||||
// const innerEnv = makeInnerEnv(env, name, suggestion);
|
||||
// const [resolved] = evalExprBlock(inner, innerEnv);
|
||||
// return scoreResolved(resolved, score);
|
||||
// };
|
||||
|
||||
// const [valueResolved] = evalExprBlock(value, env);
|
||||
// const innerEnv = makeInnerEnv(env, name, valueResolved);
|
||||
|
||||
const {paramType, innerEnv} = inferTypeLet(state, env);
|
||||
|
||||
return <>
|
||||
<span className="keyword column">let </span>
|
||||
<span className="column rightAlign">
|
||||
<Input
|
||||
placeholder="<name>"
|
||||
text={state.name}
|
||||
suggestion=""
|
||||
onEnter={() => {}}
|
||||
onCancel={() => {}}
|
||||
onTextChange={name => setState(state => ({...state, name}))}
|
||||
extraHandlers={{}}
|
||||
/>
|
||||
:: <Type type={paramType} />
|
||||
</span>
|
||||
<span className="keyword column"> = </span>
|
||||
<span className="column">
|
||||
<ExprBlock
|
||||
state={state.value}
|
||||
setState={setValue}
|
||||
score={() => 0}
|
||||
onCancel={() => setState(state => state.inner)} // keep inner
|
||||
/>
|
||||
</span>
|
||||
{state.inner.kind === "let" &&
|
||||
globalContext?.syntacticSugar &&
|
||||
<EnvContext value={innerEnv}>
|
||||
<DeclColumns
|
||||
state={state.inner}
|
||||
setState={setInner}
|
||||
score={score}
|
||||
/>
|
||||
</EnvContext>
|
||||
}
|
||||
</>;
|
||||
}
|
||||
|
||||
function InnerMost({state, setState, score}) {
|
||||
const env = useContext(EnvContext);
|
||||
const globalContext = useContext(GlobalContext);
|
||||
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
|
||||
// const [valueResolved] = evalExprBlock(state.value, env);
|
||||
// const innerEnv = makeInnerEnv(env, state.name, valueResolved);
|
||||
const {paramType, innerEnv} = inferTypeLet(state, env);
|
||||
const onCancel = () => setState(state => state.value);
|
||||
if (state.inner.kind === "let" && globalContext?.syntacticSugar) {
|
||||
return <EnvContext value={innerEnv}>
|
||||
<InnerMost
|
||||
state={state.inner}
|
||||
setState={setInner}
|
||||
score={score}
|
||||
/>
|
||||
</EnvContext>;
|
||||
}
|
||||
else {
|
||||
return <EnvContext value={innerEnv}>
|
||||
<ExprBlock
|
||||
state={state.inner}
|
||||
setState={setInner}
|
||||
score={score}
|
||||
onCancel={onCancel} // keep value
|
||||
/>
|
||||
</EnvContext>
|
||||
}
|
||||
}
|
||||
147
src/component/other/Input.tsx
Normal file
147
src/component/other/Input.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { useEffect, useRef, type ReactNode, type KeyboardEvent, useState } from "react";
|
||||
|
||||
interface InputProps {
|
||||
placeholder: string;
|
||||
text: string;
|
||||
suggestion: string;
|
||||
onTextChange: (text: string) => void;
|
||||
|
||||
onEnter: () => void;
|
||||
onCancel: () => void;
|
||||
extraHandlers: {[key:string]: (e: KeyboardEvent) => void}
|
||||
|
||||
children?: ReactNode | ReactNode[];
|
||||
}
|
||||
|
||||
const autoInputWidth = (ref: React.RefObject<HTMLInputElement| null>, text, emptyWidth=150) => {
|
||||
if (ref.current) {
|
||||
ref.current.style.width = `${text.length === 0 ? emptyWidth : (text.length*8.0)}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function getCaretPosition(ref: React.RefObject<HTMLInputElement| null>): number {
|
||||
return ref.current?.selectionStart || 0;
|
||||
}
|
||||
|
||||
// Move caret all the way to the right in the currently focused element
|
||||
function setRightMostCaretPosition(elem) {
|
||||
const range = document.createRange();
|
||||
range.selectNode(elem);
|
||||
if (elem.lastChild) { // if no text is entered, there is no lastChild
|
||||
range.setStart(elem.lastChild, elem.textContent.length);
|
||||
range.setEnd(elem.lastChild, elem.textContent.length);
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
function focusNextElement() {
|
||||
const editable = Array.from<any>(document.querySelectorAll('input.editable'));
|
||||
const index = editable.indexOf(document.activeElement);
|
||||
const nextElem = editable[index+1];
|
||||
if (nextElem) {
|
||||
nextElem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function focusPrevElement() {
|
||||
const editable = Array.from<any>(document.querySelectorAll('input.editable'));
|
||||
const index = editable.indexOf(document.activeElement);
|
||||
const prevElem = editable[index-1]
|
||||
if (prevElem) {
|
||||
prevElem.focus();
|
||||
setRightMostCaretPosition(prevElem);
|
||||
}
|
||||
}
|
||||
|
||||
export function Input({placeholder, text, suggestion, onTextChange, onEnter, onCancel, extraHandlers, children}: InputProps) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [focus, setFocus] = useState<"yes"|"hide"|"no">("no");
|
||||
|
||||
useEffect(() => autoInputWidth(ref, (text+(focus?suggestion:'')) || placeholder), [ref, text, suggestion, focus]);
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
setFocus("yes");
|
||||
const keys = {
|
||||
// auto-complete
|
||||
Tab: () => {
|
||||
if (e.shiftKey) {
|
||||
focusPrevElement();
|
||||
e.preventDefault();
|
||||
}
|
||||
else {
|
||||
// not shift key
|
||||
if (suggestion.length > 0) {
|
||||
// complete with greyed out text
|
||||
const newText = text + suggestion;
|
||||
onTextChange(newText);
|
||||
setRightMostCaretPosition(ref.current);
|
||||
e.preventDefault();
|
||||
}
|
||||
else {
|
||||
onEnter();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
Enter: () => {
|
||||
onEnter();
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
// cancel
|
||||
Backspace: () => {
|
||||
if (text.length === 0) {
|
||||
onCancel();
|
||||
focusPrevElement();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
// navigate with arrows
|
||||
ArrowLeft: () => {
|
||||
if (getCaretPosition(ref) <= 0) {
|
||||
focusPrevElement();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
ArrowRight: () => {
|
||||
if (getCaretPosition(ref) === text.length) {
|
||||
focusNextElement();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
Escape: () => {
|
||||
if (focus === "yes") {
|
||||
setFocus("hide");
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
...extraHandlers,
|
||||
};
|
||||
const handler = keys[e.key];
|
||||
if (handler) {
|
||||
handler(e);
|
||||
}
|
||||
};
|
||||
|
||||
return <span className="inputBlock">
|
||||
{(focus === "yes") && children}
|
||||
<span className="editable suggest">{text}{focus && suggestion}</span>
|
||||
<input ref={ref}
|
||||
placeholder={placeholder}
|
||||
className="editable"
|
||||
value={text}
|
||||
onInput={(e) =>
|
||||
// @ts-ignore
|
||||
onTextChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={() => setFocus("yes")}
|
||||
onBlur={() => setFocus("no")}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</span>;
|
||||
}
|
||||
72
src/component/other/Type.css
Normal file
72
src/component/other/Type.css
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
.infix {
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.type {
|
||||
margin:1px;
|
||||
display: inline-block;
|
||||
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variation-settings: "wdth" 100;
|
||||
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.functionType {
|
||||
border: solid 1px darkred;
|
||||
background-color: bisque;
|
||||
color:darkred;
|
||||
}
|
||||
|
||||
.productType {
|
||||
border: solid 1px darkblue;
|
||||
background-color: aliceblue;
|
||||
color:darkblue;
|
||||
}
|
||||
|
||||
.sumType {
|
||||
border: solid 1px #333;
|
||||
background-color: lightyellow;
|
||||
color:#333
|
||||
}
|
||||
|
||||
.dictType {
|
||||
border-radius: 5px;
|
||||
border: solid 1px darkblue;
|
||||
background-color: rgb(206, 232, 255);
|
||||
color:darkblue
|
||||
}
|
||||
|
||||
.setType {
|
||||
border-radius: 5px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
border: solid 1px darkblue;
|
||||
background-color: rgb(206, 232, 255);
|
||||
color:darkblue
|
||||
}
|
||||
|
||||
.listType {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
border: solid 1px darkblue;
|
||||
background-color: rgb(206, 232, 255);
|
||||
color:darkblue
|
||||
}
|
||||
|
||||
.iteratorType {
|
||||
border-style: dashed;
|
||||
/* animation: flickerAnimation 500ms steps(1) normal infinite; */
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
|
||||
@keyframes flickerAnimation {
|
||||
0% { opacity:1; }
|
||||
50% { opacity:0; }
|
||||
100% { opacity:1; }
|
||||
}
|
||||
46
src/component/other/Type.tsx
Normal file
46
src/component/other/Type.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { getHumanReadableName, getSymbol, symbolDict, symbolDictIterator, symbolFunction, symbolList, symbolProduct, symbolSet, symbolSetIterator, symbolSum } from "dope2";
|
||||
|
||||
import "./Type.css";
|
||||
|
||||
export function Type({type}) {
|
||||
const symbol = getSymbol(type);
|
||||
switch (symbol) {
|
||||
case symbolFunction:
|
||||
return <BinaryType type={type} cssClass="functionType" infix="→" prefix="" suffix=""/>;
|
||||
case symbolProduct:
|
||||
return <BinaryType type={type} cssClass="productType" infix="⨯" prefix="" suffix=""/>;
|
||||
case symbolSum:
|
||||
return <BinaryType type={type} cssClass="sumType" infix="+" prefix="" suffix=""/>;
|
||||
case symbolDict:
|
||||
return <BinaryType type={type} cssClass="dictType" infix="⇒" prefix="{" suffix="}"/>;
|
||||
case symbolSet:
|
||||
return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
|
||||
case symbolList:
|
||||
return <UnaryType type={type} cssClass="listType" prefix="[" suffix="]" />;
|
||||
case symbolSetIterator:
|
||||
return <UnaryType type={type} cssClass="setType iteratorType" prefix="{*" suffix="}" />;
|
||||
case symbolDictIterator:
|
||||
return <BinaryType type={type} cssClass="dictType iteratorType" infix="*⇒" prefix="{" suffix="}"/>;
|
||||
|
||||
default:
|
||||
return <div className="type">{getHumanReadableName(symbol)}</div>
|
||||
}
|
||||
}
|
||||
|
||||
function BinaryType({type, cssClass, infix, prefix, suffix}) {
|
||||
return <div className={`type ${cssClass}`}>
|
||||
{prefix}
|
||||
<Type type={type.params[0](type)}/>
|
||||
<span className="infix">{infix}</span>
|
||||
<Type type={type.params[1](type)}/>
|
||||
{suffix}
|
||||
</div>
|
||||
}
|
||||
|
||||
function UnaryType({type, cssClass, prefix, suffix}) {
|
||||
return <div className={`type ${cssClass}`}>
|
||||
{prefix}
|
||||
<Type type={type.params[0](type)}/>
|
||||
{suffix}
|
||||
</div>
|
||||
}
|
||||
19
src/component/other/Value.css
Normal file
19
src/component/other/Value.css
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.valuePrimitive {
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.valueUUID {
|
||||
border-radius: 10px;
|
||||
background-color: lightyellow;
|
||||
border: 1px solid black;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
90
src/component/other/Value.tsx
Normal file
90
src/component/other/Value.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set, Unit, symbolType, symbolUUID, getHumanReadableName, Ordering} from "dope2";
|
||||
|
||||
import "./Value.css";
|
||||
import { Type } from "./Type";
|
||||
|
||||
export function Value({dynamic}) {
|
||||
const type = getType(dynamic);
|
||||
const inst = getInst(dynamic);
|
||||
if (eqType(type)(Double)) {
|
||||
return <ValueDouble val={inst}/>;
|
||||
}
|
||||
if (eqType(type)(Int)) {
|
||||
return <ValueInt val={inst}/>;
|
||||
}
|
||||
if (eqType(type)(Bool)) {
|
||||
return <ValueBool val={inst}/>;
|
||||
}
|
||||
if (eqType(type)(Unit)) {
|
||||
return <ValueUnit/>;
|
||||
}
|
||||
if (eqType(type)(Ordering)) {
|
||||
return <ValueOrdering val={inst}/>;
|
||||
}
|
||||
|
||||
const symbol = getSymbol(type);
|
||||
switch (symbol) {
|
||||
case symbolFunction:
|
||||
return <ValueFunction/>;
|
||||
case symbolSum:
|
||||
return <ValueSum val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>;
|
||||
case symbolProduct:
|
||||
return <ValueProduct val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>;
|
||||
case symbolDict:
|
||||
return <ValueDict val={inst} keyType={type.params[0](type)} valueType={type.params[1](type)}/>;
|
||||
case symbolSet:
|
||||
return <ValueSet val={inst} elemType={type.params[0](type)} />;
|
||||
case symbolList:
|
||||
return <ValueList val={inst} elemType={type.params[0](type)} />;
|
||||
case symbolType:
|
||||
return <Type type={inst}/>;
|
||||
case symbolUUID:
|
||||
return <ValueUUID val={inst}/>
|
||||
default:
|
||||
console.log("don't know how to show value:", dynamic);
|
||||
return <>don't know how to show value</>;
|
||||
}
|
||||
}
|
||||
|
||||
function ValueDouble({val}) {
|
||||
return <span className="valuePrimitive">{val.toString()}</span>;
|
||||
}
|
||||
function ValueInt({val}) {
|
||||
return <span className="valuePrimitive">{val.toString()}</span>;
|
||||
}
|
||||
function ValueFunction() {
|
||||
return <>𝑓𝑛 </>;
|
||||
}
|
||||
function ValueBool({val}) {
|
||||
return <span className="valuePrimitive">{val.toString()}</span>;
|
||||
}
|
||||
function ValueList({val, elemType}) {
|
||||
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
|
||||
}
|
||||
function ValueSet({val, elemType}) {
|
||||
return <span className="setType">{'{'}{set.fold(acc => elem => acc.concat([elem]))([])(val).map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}{'}'}</span>;
|
||||
}
|
||||
function ValueDict({val, keyType, valueType}) {
|
||||
return <span className="dictType">{'{'}{dict.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => <span key={i}>
|
||||
<Value dynamic={{i:key, t:keyType}}/>
|
||||
⇒
|
||||
<Value dynamic={{i:value, t:valueType}}/>
|
||||
</span>)}{'}'}</span>;
|
||||
}
|
||||
function ValueSum({val, leftType, rightType}) {
|
||||
return match(val)
|
||||
(l => <span className="sumType">L <Value dynamic={{i:l, t:leftType}}/></span>)
|
||||
(r => <span className="sumType">R <Value dynamic={{i:r, t:rightType}}/></span>);
|
||||
}
|
||||
function ValueProduct({val, leftType, rightType}) {
|
||||
return <span className="productType">(<Value dynamic={{i:getLeft(val), t:leftType}}/>, <Value dynamic={{i:getRight(val), t:rightType}} />)</span>;
|
||||
}
|
||||
function ValueUnit() {
|
||||
return <>{'()'}</>;
|
||||
}
|
||||
function ValueUUID({val}) {
|
||||
return <span className="valueUUID">{getHumanReadableName(val)}</span>;
|
||||
}
|
||||
function ValueOrdering({val}) {
|
||||
return <span className="valueOrdering">{{[-1]: "LessThan", [0]: "Equal", [1]: "GreaterThan" }[val]}</span>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue