greatly simplify state + cleanup code
This commit is contained in:
parent
2d0deca127
commit
5964510036
11 changed files with 268 additions and 321 deletions
|
|
@ -57,6 +57,6 @@ footer a {
|
||||||
}
|
}
|
||||||
|
|
||||||
.factoryReset {
|
.factoryReset {
|
||||||
background-color: red;
|
background-color: rgb(255, 0, 0);
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
111
src/App.tsx
111
src/App.tsx
|
|
@ -1,11 +1,10 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import './App.css'
|
import './App.css';
|
||||||
import { Editor, type EditorState } from './Editor'
|
|
||||||
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
|
|
||||||
import { CommandContext } from './CommandContext';
|
import { CommandContext } from './CommandContext';
|
||||||
import { deserialize, serialize } from './types';
|
import { Editor, type EditorState } from './Editor';
|
||||||
import { extendedEnv } from './EnvContext';
|
import { extendedEnv } from './EnvContext';
|
||||||
import { useEffectBetter } from './util/use_effect_better';
|
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
|
||||||
|
import { evalEditorBlock } from "./eval";
|
||||||
|
|
||||||
const commands: [string, string[], string][] = [
|
const commands: [string, string[], string][] = [
|
||||||
["call" , ['c' ], "call" ],
|
["call" , ['c' ], "call" ],
|
||||||
|
|
@ -19,54 +18,77 @@ const examples: [string, EditorState][] = [
|
||||||
["push to list", nonEmptyEditorState],
|
["push to list", nonEmptyEditorState],
|
||||||
["function w/ 4 params", tripleFunctionCallEditorState]];
|
["function w/ 4 params", tripleFunctionCallEditorState]];
|
||||||
|
|
||||||
|
type AppState = {
|
||||||
|
history: EditorState[],
|
||||||
|
future: EditorState[],
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
export function App() {
|
||||||
// const [history, setHistory] = useState([initialEditorState]);
|
|
||||||
// const [history, setHistory] = useState([nonEmptyEditorState]);
|
|
||||||
// const [history, setHistory] = useState([tripleFunctionCallEditorState]);
|
|
||||||
// const [future, setFuture] = useState<EditorState[]>([]);
|
|
||||||
|
|
||||||
// load from localStorage
|
// load from localStorage
|
||||||
const [history, setHistory] = useState<EditorState[]>(
|
const [appState, setAppState] = useState(loadFromLocalStorage());
|
||||||
localStorage["history"]
|
|
||||||
? JSON.parse(localStorage["history"]).map(s => deserialize(s, extendedEnv))
|
|
||||||
: [initialEditorState]
|
|
||||||
);
|
|
||||||
const [future, setFuture] = useState<EditorState[]>(
|
|
||||||
localStorage["future"]
|
|
||||||
? JSON.parse(localStorage["future"]).map(s => deserialize(s, extendedEnv))
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffectBetter(() => {
|
useEffect(() => {
|
||||||
// persist accross reloads
|
// persist accross reloads
|
||||||
localStorage["history"] = JSON.stringify(history.map(serialize));
|
localStorage["appState"] = JSON.stringify(appState);
|
||||||
localStorage["future"] = JSON.stringify(future.map(serialize));
|
}, [appState]);
|
||||||
}, [history, future]);
|
|
||||||
|
const factoryReset = () => {
|
||||||
|
setAppState(_ => defaultState);
|
||||||
|
}
|
||||||
|
|
||||||
const pushHistory = (callback: (p: EditorState) => EditorState) => {
|
const pushHistory = (callback: (p: EditorState) => EditorState) => {
|
||||||
const newState = callback(history.at(-1)!);
|
setAppState(({history}) => {
|
||||||
setHistory(history.concat([newState]));
|
const newState = callback(history.at(-1)!);
|
||||||
setFuture([]);
|
return {
|
||||||
|
history: history.concat([newState]),
|
||||||
|
future: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUndo = () => {
|
const onUndo = () => {
|
||||||
setFuture(future.concat(history.at(-1)!)); // add
|
setAppState(({history, future}) => ({
|
||||||
setHistory(history.slice(0,-1)); // remove
|
history: history.slice(0,-1),
|
||||||
|
future: future.concat(history.at(-1)!),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
const onRedo = () => {
|
const onRedo = () => {
|
||||||
setHistory(history.concat(future.at(-1)!)); // add
|
setAppState(({history, future}) => ({
|
||||||
setFuture(future.slice(0,-1)); // remove
|
history: history.concat(future.at(-1)!),
|
||||||
|
future: future.slice(0,-1),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (e) => {
|
const onKeyDown = (e) => {
|
||||||
if (e.key === "Z" && e.ctrlKey) {
|
if (e.key === "Z" && e.ctrlKey) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
if (future.length > 0) {
|
if (appState.future.length > 0) {
|
||||||
onRedo();
|
onRedo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (history.length > 1) {
|
if (appState.history.length > 1) {
|
||||||
onUndo();
|
onUndo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,8 +97,8 @@ export function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window['APP_STATE'] = history; // useful for debugging
|
window['APP_STATE'] = appState.history; // useful for debugging
|
||||||
}, [history]);
|
}, [appState.history]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.onkeydown = onKeyDown;
|
window.onkeydown = onKeyDown;
|
||||||
|
|
@ -95,14 +117,17 @@ export function App() {
|
||||||
|
|
||||||
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
pushHistory(_ => examples[e.target.value][1]);
|
if (e.target.value) {
|
||||||
|
// @ts-ignore
|
||||||
|
pushHistory(_ => examples[e.target.value][1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
|
<button disabled={appState.history.length===1} onClick={onUndo}>Undo ({appState.history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
|
||||||
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</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>
|
||||||
{
|
{
|
||||||
commands.map(([_, keys, descr], i) =>
|
commands.map(([_, keys, descr], i) =>
|
||||||
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
|
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
|
||||||
|
|
@ -111,22 +136,22 @@ export function App() {
|
||||||
</span>)
|
</span>)
|
||||||
}
|
}
|
||||||
<select onClick={onSelectExample}>
|
<select onClick={onSelectExample}>
|
||||||
|
<option>load example...</option>
|
||||||
{
|
{
|
||||||
examples.map(([name], i) => {
|
examples.map(([name], i) => {
|
||||||
return <option key={i} value={i}>{name}</option>;
|
return <option key={i} value={i}>{name}</option>;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<button className="factoryReset" onClick={() => {
|
<button className="factoryReset" onClick={factoryReset}>
|
||||||
setHistory(_ => [initialEditorState]);
|
FACTORY RESET
|
||||||
setFuture(_ => []);
|
</button>
|
||||||
}}>FACTORY RESET</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main onKeyDown={onKeyDown}>
|
<main onKeyDown={onKeyDown}>
|
||||||
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight}}>
|
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight}}>
|
||||||
<Editor
|
<Editor
|
||||||
state={history.at(-1)!}
|
state={appState.history.at(-1)!}
|
||||||
setState={pushHistory}
|
setState={pushHistory}
|
||||||
onCancel={() => {}}
|
onCancel={() => {}}
|
||||||
filter={() => true}
|
filter={() => true}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction, UnifyError } from "dope2";
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { Editor, type EditorState } from "./Editor";
|
import { Editor, type EditorState } from "./Editor";
|
||||||
import { Value } from "./Value";
|
import { Value } from "./Value";
|
||||||
import { DeepError, type ResolvedType, type SetStateFn, type State2Props } from "./types";
|
import { type SetStateFn, type State2Props } from "./Editor";
|
||||||
|
import { DeepError, evalCallBlock, evalEditorBlock } from "./eval";
|
||||||
import { useEffect } from "react";
|
import { type ResolvedType } from "./eval";
|
||||||
import "./CallBlock.css";
|
import "./CallBlock.css";
|
||||||
import { useEffectBetter } from "./util/use_effect_better";
|
import { EnvContext } from "./EnvContext";
|
||||||
|
import type { SuggestionType } from "./InputBlock";
|
||||||
|
|
||||||
export interface CallBlockState<
|
export interface CallBlockState<
|
||||||
FnState=EditorState,
|
FnState=EditorState,
|
||||||
|
|
@ -15,7 +16,6 @@ export interface CallBlockState<
|
||||||
kind: "call";
|
kind: "call";
|
||||||
fn: FnState;
|
fn: FnState;
|
||||||
input: InputState;
|
input: InputState;
|
||||||
resolved: ResolvedType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CallBlockProps<
|
interface CallBlockProps<
|
||||||
|
|
@ -24,49 +24,13 @@ interface CallBlockProps<
|
||||||
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
|
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveCallBlock(fn: ResolvedType, input: ResolvedType) {
|
function headlessCallBlock({setState}: CallBlockProps) {
|
||||||
if (have(input) && have(fn)) {
|
|
||||||
try {
|
|
||||||
const outputResolved = apply(input)(fn); // may throw
|
|
||||||
return outputResolved; // success
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return new DeepError(e, 0); // eval error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (input instanceof DeepError) {
|
|
||||||
return input; // bubble up the error
|
|
||||||
}
|
|
||||||
else if (fn instanceof DeepError) {
|
|
||||||
return new DeepError(fn.e, fn.depth+1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function have(resolved: ResolvedType) {
|
|
||||||
return resolved && !(resolved instanceof DeepError);
|
|
||||||
}
|
|
||||||
|
|
||||||
function headlessCallBlock({state, setState}: CallBlockProps) {
|
|
||||||
const {fn, input} = state;
|
|
||||||
const setFn = (callback: SetStateFn) => {
|
const setFn = (callback: SetStateFn) => {
|
||||||
setState(state => ({...state, fn: callback(state.fn)}));
|
setState(state => ({...state, fn: callback(state.fn)}));
|
||||||
}
|
}
|
||||||
const setInput = (callback: SetStateFn) => {
|
const setInput = (callback: SetStateFn) => {
|
||||||
setState(state => ({...state, input: callback(state.input)}));
|
setState(state => ({...state, input: callback(state.input)}));
|
||||||
}
|
}
|
||||||
const setResolved = (callback: SetStateFn<ResolvedType>) => {
|
|
||||||
setState(state => ({...state, resolved: callback(state.resolved)}));
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffectBetter(() => {
|
|
||||||
// Here we do something spooky: we update the state in response to state change...
|
|
||||||
// The reason this shouldn't give problems is because we update the state in such a way that the changes only 'trickle up', rather than getting stuck in a cycle.
|
|
||||||
setResolved(() => resolveCallBlock(fn.resolved, input.resolved));
|
|
||||||
}, [fn.resolved, input.resolved]);
|
|
||||||
|
|
||||||
const onFnCancel = () => {
|
const onFnCancel = () => {
|
||||||
setState(state => state.input); // we become our input
|
setState(state => state.input); // we become our input
|
||||||
}
|
}
|
||||||
|
|
@ -78,8 +42,10 @@ function headlessCallBlock({state, setState}: CallBlockProps) {
|
||||||
|
|
||||||
export function CallBlock({ state, setState }: CallBlockProps) {
|
export function CallBlock({ state, setState }: CallBlockProps) {
|
||||||
const {setFn, setInput, onFnCancel, onInputCancel}
|
const {setFn, setInput, onFnCancel, onInputCancel}
|
||||||
= headlessCallBlock({ state, setState });
|
= headlessCallBlock({ state, setState });
|
||||||
return <span className={"functionBlock" + ((state.resolved instanceof DeepError) ? " unifyError" : "")}>
|
const env = useContext(EnvContext);
|
||||||
|
const resolved = evalEditorBlock(state, env);
|
||||||
|
return <span className={"functionBlock" + ((resolved instanceof DeepError) ? " unifyError" : "")}>
|
||||||
<FunctionHeader
|
<FunctionHeader
|
||||||
fn={state.fn}
|
fn={state.fn}
|
||||||
setFn={setFn}
|
setFn={setFn}
|
||||||
|
|
@ -93,12 +59,11 @@ export function CallBlock({ state, setState }: CallBlockProps) {
|
||||||
input={state.input} setInput={setInput}
|
input={state.input} setInput={setInput}
|
||||||
onInputCancel={onInputCancel}
|
onInputCancel={onInputCancel}
|
||||||
depth={0}
|
depth={0}
|
||||||
errorDepth={state.resolved instanceof DeepError ? (state.resolved.depth) : -1}
|
errorDepth={resolved instanceof DeepError ? (resolved.depth) : -1}
|
||||||
/>
|
/>
|
||||||
{/* Output (or Error) */}
|
{/* Output (or Error) */}
|
||||||
{ state.resolved instanceof DeepError && state.resolved.e.toString()
|
{ resolved instanceof DeepError && resolved.e.toString()
|
||||||
|| state.resolved && <><Value dynamic={state.resolved} />
|
|| resolved && <><Value dynamic={resolved} />
|
||||||
{/* ☑ */}
|
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,22 +71,8 @@ export function CallBlock({ state, setState }: CallBlockProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterFnInputs(fn: ResolvedType, input: ResolvedType) {
|
function filterFnInputs(fn: ResolvedType, input: ResolvedType) {
|
||||||
if (!have(fn) || !have(input)) {
|
const resolved = evalCallBlock(fn, input);
|
||||||
return false;
|
return (resolved && !(resolved instanceof Error));
|
||||||
}
|
|
||||||
const fnType = getType(fn);
|
|
||||||
if (getSymbol(fnType) !== symbolFunction) {
|
|
||||||
return false; // filter out non-functions already
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
assignFn(fnType, getType(input)); // may throw
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
if (!(e instanceof UnifyError)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FunctionHeader({ fn, setFn, input, onFnCancel }) {
|
function FunctionHeader({ fn, setFn, input, onFnCancel }) {
|
||||||
|
|
@ -142,6 +93,7 @@ function FunctionHeader({ fn, setFn, input, onFnCancel }) {
|
||||||
input={fn.input} />;
|
input={fn.input} />;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const env = useContext(EnvContext);
|
||||||
// end of recursion - draw function name
|
// end of recursion - draw function name
|
||||||
return <span className="functionName">
|
return <span className="functionName">
|
||||||
𝑓𝑛
|
𝑓𝑛
|
||||||
|
|
@ -149,12 +101,13 @@ function FunctionHeader({ fn, setFn, input, onFnCancel }) {
|
||||||
state={fn}
|
state={fn}
|
||||||
setState={setFn}
|
setState={setFn}
|
||||||
onCancel={onFnCancel}
|
onCancel={onFnCancel}
|
||||||
filter={([_, fnCandidate]) => filterFnInputs(fnCandidate, input.resolved)} />
|
filter={(fnSuggestion: SuggestionType) => filterFnInputs(fnSuggestion[2], evalEditorBlock(input, env))} />
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth }) {
|
function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth }) {
|
||||||
|
const env = useContext(EnvContext);
|
||||||
return <div className={"inputParam" + (depth === errorDepth ? " offending" : "")}>
|
return <div className={"inputParam" + (depth === errorDepth ? " offending" : "")}>
|
||||||
{(fn.kind === "call") &&
|
{(fn.kind === "call") &&
|
||||||
// if the function we're calling is itself the result of a function call,
|
// if the function we're calling is itself the result of a function call,
|
||||||
|
|
@ -168,7 +121,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
|
||||||
state={input}
|
state={input}
|
||||||
setState={setInput}
|
setState={setInput}
|
||||||
onCancel={onInputCancel}
|
onCancel={onInputCancel}
|
||||||
filter={([_, inputCandidate]) => filterFnInputs(fn.resolved, inputCandidate)}
|
filter={(inputSuggestion: SuggestionType) => filterFnInputs(evalEditorBlock(fn, env), inputSuggestion[2])}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { getSymbol, getType, symbolFunction } from "dope2";
|
import { getSymbol, getType, symbolFunction } from "dope2";
|
||||||
|
|
||||||
import { useContext, useEffect, useReducer, useRef, useState } from "react";
|
|
||||||
import { CallBlock, type CallBlockState } from "./CallBlock";
|
import { CallBlock, type CallBlockState } from "./CallBlock";
|
||||||
import { InputBlock, type InputBlockState } from "./InputBlock";
|
import { InputBlock, type InputBlockState, type SuggestionType } from "./InputBlock";
|
||||||
import { Type } from "./Type";
|
import { Type } from "./Type";
|
||||||
import { DeepError, type Dynamic, type State2Props } from "./types";
|
import { DeepError, evalEditorBlock } from "./eval";
|
||||||
|
|
||||||
import "./Editor.css";
|
|
||||||
import { LetInBlock, type LetInBlockState } from "./LetInBlock";
|
|
||||||
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
|
|
||||||
import type { LambdaBlockState } from "./LambdaBlock";
|
|
||||||
import { initialEditorState } from "./configurations";
|
|
||||||
import { CommandContext } from "./CommandContext";
|
import { CommandContext } from "./CommandContext";
|
||||||
|
import "./Editor.css";
|
||||||
|
import { EnvContext } from "./EnvContext";
|
||||||
|
import type { LambdaBlockState } from "./LambdaBlock";
|
||||||
|
import { LetInBlock, type LetInBlockState } from "./LetInBlock";
|
||||||
|
import { initialEditorState } from "./configurations";
|
||||||
|
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
|
||||||
|
|
||||||
export type EditorState =
|
export type EditorState =
|
||||||
InputBlockState
|
InputBlockState
|
||||||
|
|
@ -19,8 +20,15 @@ export type EditorState =
|
||||||
| LetInBlockState
|
| LetInBlockState
|
||||||
| LambdaBlockState;
|
| LambdaBlockState;
|
||||||
|
|
||||||
|
export type SetStateFn<InType = EditorState, OutType = InType> = (state: InType) => OutType;
|
||||||
|
|
||||||
|
export interface State2Props<InType, OutType = InType> {
|
||||||
|
state: InType;
|
||||||
|
setState: (callback: SetStateFn<InType, OutType>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface EditorProps extends State2Props<EditorState> {
|
interface EditorProps extends State2Props<EditorState> {
|
||||||
filter: (suggestion: [string, Dynamic]) => boolean;
|
filter: (suggestion: SuggestionType) => boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +60,7 @@ function removeFocus(state: EditorState): EditorState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
|
const env = useContext(EnvContext);
|
||||||
const [needCommand, setNeedCommand] = useState(false);
|
const [needCommand, setNeedCommand] = useState(false);
|
||||||
const commandInputRef = useRef<HTMLInputElement>(null);
|
const commandInputRef = useRef<HTMLInputElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -59,21 +68,6 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
commandInputRef.current?.focus();
|
commandInputRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [needCommand]);
|
}, [needCommand]);
|
||||||
// const onMyResolve = (editorState: EditorState) => {
|
|
||||||
// setState(editorState);
|
|
||||||
// onResolve(editorState);
|
|
||||||
// return;
|
|
||||||
|
|
||||||
|
|
||||||
// if (editorState.resolved) {
|
|
||||||
// setNeedCommand(true);
|
|
||||||
// }
|
|
||||||
// else {
|
|
||||||
// // unresolved
|
|
||||||
// setNeedCommand(false);
|
|
||||||
// onResolve(editorState); // pass up the fact that we're unresolved
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const globalContext = useContext(CommandContext);
|
const globalContext = useContext(CommandContext);
|
||||||
const onCommand = (e: React.KeyboardEvent) => {
|
const onCommand = (e: React.KeyboardEvent) => {
|
||||||
|
|
@ -167,12 +161,13 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const resolved = evalEditorBlock(state, env);
|
||||||
return <>
|
return <>
|
||||||
{renderBlock()}
|
{renderBlock()}
|
||||||
{
|
{
|
||||||
(state.resolved && !(state.resolved instanceof DeepError))
|
(resolved && !(resolved instanceof DeepError))
|
||||||
? <div className="typeSignature">
|
? <div className="typeSignature">
|
||||||
:: <Type type={getType(state.resolved)} />
|
:: <Type type={getType(resolved)} />
|
||||||
</div>
|
</div>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,49 @@
|
||||||
import { Double, getType, Int, newDynamic, trie } from "dope2";
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { Double, getType, Int, newDynamic, prettyT, trie } from "dope2";
|
||||||
|
|
||||||
|
import { EnvContext } from "./EnvContext";
|
||||||
|
import type { Dynamic } from "./eval";
|
||||||
|
import "./InputBlock.css";
|
||||||
|
import { Type } from "./Type";
|
||||||
|
import type { State2Props } from "./Editor";
|
||||||
import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
|
import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
|
||||||
import { parseDouble, parseInt } from "./util/parse";
|
import { parseDouble, parseInt } from "./util/parse";
|
||||||
|
|
||||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
interface Literal {
|
||||||
import { Type } from "./Type";
|
kind: "literal";
|
||||||
|
type: string; // todo: store (and serialize) real type
|
||||||
import "./InputBlock.css";
|
};
|
||||||
import type { Dynamic, State2Props } from "./types";
|
interface Name {
|
||||||
import { EnvContext } from "./EnvContext";
|
kind: "name";
|
||||||
|
}
|
||||||
|
interface Text {
|
||||||
|
kind: "text";
|
||||||
|
}
|
||||||
|
export type InputValueType = Literal | Name | Text;
|
||||||
|
|
||||||
export interface InputBlockState {
|
export interface InputBlockState {
|
||||||
kind: "input";
|
kind: "input";
|
||||||
text: string;
|
text: string;
|
||||||
resolved: undefined | Dynamic;
|
value: InputValueType;
|
||||||
focus: boolean
|
focus: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SuggestionType = ['literal'|'name', string, Dynamic];
|
||||||
|
|
||||||
interface InputBlockProps extends State2Props<InputBlockState> {
|
interface InputBlockProps extends State2Props<InputBlockState> {
|
||||||
filter: (suggestion: [string, Dynamic]) => boolean;
|
filter: (suggestion: SuggestionType) => boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeSuggestions = (text, env, filter) => {
|
const computeSuggestions = (text, env, filter): SuggestionType[] => {
|
||||||
const asDouble = parseDouble(text);
|
const asDouble = parseDouble(text);
|
||||||
const asInt = parseInt(text);
|
const asInt = parseInt(text);
|
||||||
|
|
||||||
const ls = [
|
const ls = [
|
||||||
... (asDouble ? [[asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
|
... (asDouble ? [["literal", asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
|
||||||
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
... (asInt ? [["literal", asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
||||||
... trie.suggest(env.name2dyn)(text)(Infinity),
|
... trie.suggest(env.name2dyn)(text)(Infinity).map(([name,type]) => ["name", name, type]),
|
||||||
]
|
]
|
||||||
// return ls;
|
// return ls;
|
||||||
return [
|
return [
|
||||||
|
|
@ -39,7 +54,7 @@ const computeSuggestions = (text, env, filter) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputBlock({ state, setState, filter, onCancel }: InputBlockProps) {
|
export function InputBlock({ state, setState, filter, onCancel }: InputBlockProps) {
|
||||||
const {text, resolved, focus} = state;
|
const {text, focus} = state;
|
||||||
const env = useContext(EnvContext);
|
const env = useContext(EnvContext);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [i, setI] = useState(0); // selected suggestion idx
|
const [i, setI] = useState(0); // selected suggestion idx
|
||||||
|
|
@ -68,7 +83,7 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
|
|
||||||
const onTextChange = newText => {
|
const onTextChange = newText => {
|
||||||
const found = trie.get(env.name2dyn)(newText);
|
const found = trie.get(env.name2dyn)(newText);
|
||||||
setState(state => ({...state, text: newText, resolved: found}));
|
setState(state => ({...state, text: newText}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// fired before onInput
|
// fired before onInput
|
||||||
|
|
@ -131,8 +146,21 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
onTextChange(e.target.value);
|
onTextChange(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectSuggestion = ([name, dynamic]) => {
|
const onSelectSuggestion = ([kind, name, dynamic]: SuggestionType) => {
|
||||||
setState(state => ({...state, text: name, resolved: dynamic}));
|
if (kind === "literal") {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
text: name,
|
||||||
|
value: {kind, type: prettyT(getType(dynamic))},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
text: name,
|
||||||
|
value: {kind},
|
||||||
|
}))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <span>
|
return <span>
|
||||||
|
|
@ -158,7 +186,6 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
spellCheck={false}/>
|
spellCheck={false}/>
|
||||||
{/* Single 'grey' suggestion */}
|
{/* Single 'grey' suggestion */}
|
||||||
<span className="text-block suggest">{singleSuggestion}</span>
|
<span className="text-block suggest">{singleSuggestion}</span>
|
||||||
{/* { resolved && <>☑</>} */}
|
|
||||||
</span>
|
</span>
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
@ -173,13 +200,13 @@ function Suggestions({ suggestions, onSelect, i, setI }) {
|
||||||
};
|
};
|
||||||
return <>{(suggestions.length > 0) &&
|
return <>{(suggestions.length > 0) &&
|
||||||
<div className={"suggestions"}>
|
<div className={"suggestions"}>
|
||||||
{suggestions.map(([name, dynamic], j) =>
|
{suggestions.map(([kind, name, dynamic], j) =>
|
||||||
<div
|
<div
|
||||||
key={`${j}_${name}`}
|
key={`${j}_${name}`}
|
||||||
className={(i === j ? " selected" : "")}
|
className={(i === j ? " selected" : "")}
|
||||||
onMouseEnter={onMouseEnter(j)}
|
onMouseEnter={onMouseEnter(j)}
|
||||||
onMouseDown={onMouseDown(j)}>
|
onMouseDown={onMouseDown(j)}>
|
||||||
{name} :: <Type type={getType(dynamic)} />
|
({kind}) {name} :: <Type type={getType(dynamic)} />
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
}</>;
|
}</>;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { EditorState } from "./Editor";
|
import type { EditorState } from "./Editor";
|
||||||
import type { Dynamic, ResolvedType } from "./types";
|
import type { Dynamic } from "./eval";
|
||||||
|
import type { ResolvedType } from "./eval";
|
||||||
|
|
||||||
|
|
||||||
export interface LambdaBlockState {
|
export interface LambdaBlockState {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import { useContext, useEffect, useRef } from "react";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { growEnv } from "dope2";
|
||||||
|
|
||||||
import { Editor, type EditorState } from "./Editor";
|
import { Editor, type EditorState } from "./Editor";
|
||||||
import { EnvContext } from "./EnvContext";
|
import { EnvContext } from "./EnvContext";
|
||||||
import { DeepError, type ResolvedType, type State2Props } from "./types";
|
import { DeepError, evalEditorBlock, type ResolvedType } from "./eval";
|
||||||
import { growEnv } from "dope2";
|
import { type State2Props } from "./Editor";
|
||||||
import { autoInputWidth } from "./util/dom_trickery";
|
import { autoInputWidth } from "./util/dom_trickery";
|
||||||
|
|
||||||
import "./LetInBlock.css";
|
import "./LetInBlock.css";
|
||||||
import { useEffectBetter } from "./util/use_effect_better";
|
|
||||||
|
|
||||||
export interface LetInBlockState {
|
export interface LetInBlockState {
|
||||||
kind: "let";
|
kind: "let";
|
||||||
name: string;
|
name: string;
|
||||||
value: EditorState;
|
value: EditorState;
|
||||||
inner: EditorState;
|
inner: EditorState;
|
||||||
resolved: ResolvedType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LetInBlockProps extends State2Props<LetInBlockState> {
|
interface LetInBlockProps extends State2Props<LetInBlockState> {
|
||||||
|
|
@ -29,7 +30,8 @@ export function makeInnerEnv(env, name: string, value: ResolvedType) {
|
||||||
export function LetInBlock({state, setState}: LetInBlockProps) {
|
export function LetInBlock({state, setState}: LetInBlockProps) {
|
||||||
const {name, value, inner} = state;
|
const {name, value, inner} = state;
|
||||||
const env = useContext(EnvContext);
|
const env = useContext(EnvContext);
|
||||||
const innerEnv = makeInnerEnv(env, name, value.resolved);
|
const valueResolved = evalEditorBlock(value, env);
|
||||||
|
const innerEnv = makeInnerEnv(env, name, valueResolved);
|
||||||
const nameRef = useRef<HTMLInputElement>(null);
|
const nameRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
|
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
|
||||||
|
|
@ -43,11 +45,6 @@ export function LetInBlock({state, setState}: LetInBlockProps) {
|
||||||
nameRef.current?.focus();
|
nameRef.current?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffectBetter(() => {
|
|
||||||
// bubble up
|
|
||||||
setState(state => ({...state, resolved: inner.resolved}));
|
|
||||||
}, [inner.resolved])
|
|
||||||
|
|
||||||
useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]);
|
useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
import { apply, Int, trie } from "dope2";
|
|
||||||
import type { EditorState } from "./Editor";
|
import type { EditorState } from "./Editor";
|
||||||
import { extendedEnv } from "./EnvContext";
|
|
||||||
|
|
||||||
export const initialEditorState: EditorState = {
|
export const initialEditorState: EditorState = {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "",
|
text: "",
|
||||||
resolved: undefined,
|
value: { kind: "text" },
|
||||||
focus: true,
|
focus: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const listPush = trie.get(extendedEnv.name2dyn)("list.push");
|
|
||||||
const listEmptyList = trie.get(extendedEnv.name2dyn)("list.emptyList");
|
|
||||||
const fourtyTwo = { i: 42n, t: Int };
|
|
||||||
|
|
||||||
export const nonEmptyEditorState: EditorState = {
|
export const nonEmptyEditorState: EditorState = {
|
||||||
kind: "call",
|
kind: "call",
|
||||||
fn: {
|
fn: {
|
||||||
|
|
@ -20,33 +14,24 @@ export const nonEmptyEditorState: EditorState = {
|
||||||
fn: {
|
fn: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "list.push",
|
text: "list.push",
|
||||||
resolved: listPush,
|
value: { kind: "name" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "list.emptyList",
|
text: "list.emptyList",
|
||||||
resolved: listEmptyList,
|
value: { kind: "name" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
resolved: apply(listEmptyList)(listPush),
|
|
||||||
// focus: ,
|
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "42",
|
text: "42",
|
||||||
resolved: fourtyTwo,
|
value: { kind: "literal", type: "Int" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
// resolved: apply(fourtyTwo)(apply(listEmptyList)(listPush)),
|
|
||||||
resolved: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const functionWith4Params = trie.get(extendedEnv.name2dyn)("functionWith4Params");
|
|
||||||
const fourtyThree = { i: 43n, t: Int };
|
|
||||||
const fourtyFour = { i: 44n, t: Int };
|
|
||||||
const fourtyFive = { i: 45n, t: Int };
|
|
||||||
|
|
||||||
export const tripleFunctionCallEditorState: EditorState = {
|
export const tripleFunctionCallEditorState: EditorState = {
|
||||||
kind: "call",
|
kind: "call",
|
||||||
fn: {
|
fn: {
|
||||||
|
|
@ -58,38 +43,34 @@ export const tripleFunctionCallEditorState: EditorState = {
|
||||||
fn: {
|
fn: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "functionWith4Params",
|
text: "functionWith4Params",
|
||||||
resolved: functionWith4Params,
|
value: { kind: "name" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "42",
|
text: "42",
|
||||||
resolved: fourtyTwo,
|
value: { kind: "literal", type: "Int" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
resolved: apply(fourtyTwo)(functionWith4Params),
|
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "43",
|
text: "43",
|
||||||
resolved: fourtyThree,
|
value: { kind: "literal", type: "Int" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
resolved: apply(fourtyThree)(apply(fourtyTwo)(functionWith4Params)),
|
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "44",
|
text: "44",
|
||||||
resolved: fourtyFour,
|
value: { kind: "literal", type: "Int" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
resolved: apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith4Params))),
|
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
text: "45",
|
text: "45",
|
||||||
resolved: fourtyFive,
|
value: { kind: "literal", type: "Int" },
|
||||||
focus: false,
|
focus: false,
|
||||||
},
|
},
|
||||||
resolved: apply(fourtyFive)(apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith4Params)))),
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
92
src/eval.ts
Normal file
92
src/eval.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { apply, Double, Int, NotAFunctionError, trie, UnifyError } from "dope2";
|
||||||
|
|
||||||
|
import type { EditorState } from "./Editor";
|
||||||
|
import type { InputValueType } from "./InputBlock";
|
||||||
|
import { makeInnerEnv } from "./LetInBlock";
|
||||||
|
import { parseDouble, parseInt } from "./util/parse";
|
||||||
|
|
||||||
|
export class DeepError {
|
||||||
|
e: Error;
|
||||||
|
depth: number;
|
||||||
|
constructor(e, depth) {
|
||||||
|
this.e = e;
|
||||||
|
this.depth = depth;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// a dynamically typed value = tuple (instance, type)
|
||||||
|
export interface Dynamic {
|
||||||
|
i: any;
|
||||||
|
t: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
// the value of every block is either known (Dynamic), an error, or unknown
|
||||||
|
export type ResolvedType = Dynamic | DeepError | undefined;
|
||||||
|
|
||||||
|
export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
|
||||||
|
if (s.kind === "input") {
|
||||||
|
return evalInputBlock(s.text, s.value, env);
|
||||||
|
}
|
||||||
|
if (s.kind === "call") {
|
||||||
|
const fn = evalEditorBlock(s.fn, env);
|
||||||
|
const input = evalEditorBlock(s.input, env);
|
||||||
|
return evalCallBlock(fn, input);
|
||||||
|
}
|
||||||
|
if (s.kind === "let") {
|
||||||
|
const value = evalEditorBlock(s.value, env);
|
||||||
|
const innerEnv = makeInnerEnv(env, s.name, value)
|
||||||
|
return evalEditorBlock(s.inner, innerEnv);
|
||||||
|
}
|
||||||
|
if (s.kind === "lambda") {
|
||||||
|
const expr = evalEditorBlock(s.expr, env);
|
||||||
|
return undefined; // todo
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType {
|
||||||
|
if (value.kind === "literal") {
|
||||||
|
return parseLiteral(text, value.type);
|
||||||
|
}
|
||||||
|
else if (value.kind === "name") {
|
||||||
|
return trie.get(env.name2dyn)(text);
|
||||||
|
}
|
||||||
|
else { // kind === "text" -> unresolved
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evalCallBlock(fn: ResolvedType, input: ResolvedType) {
|
||||||
|
if (haveValue(input) && haveValue(fn)) {
|
||||||
|
try {
|
||||||
|
const outputResolved = apply(input)(fn); // may throw
|
||||||
|
return outputResolved; // success
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return new DeepError(e, 0); // eval error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (input instanceof DeepError) {
|
||||||
|
return input; // bubble up the error
|
||||||
|
}
|
||||||
|
else if (fn instanceof DeepError) {
|
||||||
|
return new DeepError(fn.e, fn.depth+1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLiteral(text: string, type: string) {
|
||||||
|
// dirty
|
||||||
|
if (type === "Int") {
|
||||||
|
return { i: parseInt(text), t: Int };
|
||||||
|
}
|
||||||
|
if (type === "Double") {
|
||||||
|
return { i: parseDouble(text), t: Double };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function haveValue(resolved: ResolvedType) {
|
||||||
|
return resolved && !(resolved instanceof DeepError);
|
||||||
|
}
|
||||||
|
|
||||||
108
src/types.ts
108
src/types.ts
|
|
@ -1,108 +0,0 @@
|
||||||
import { Double, getDefaultTypeParser, Int, prettyT, trie } from "dope2";
|
|
||||||
import type { EditorState } from "./Editor";
|
|
||||||
import { resolveCallBlock } from "./CallBlock";
|
|
||||||
import { makeInnerEnv } from "./LetInBlock";
|
|
||||||
import { parseDouble, parseInt } from "./util/parse";
|
|
||||||
|
|
||||||
export interface Dynamic {
|
|
||||||
i: any;
|
|
||||||
t: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SetStateFn<InType=EditorState,OutType=InType> = (state: InType) => OutType;
|
|
||||||
|
|
||||||
export interface State2Props<InType,OutType=InType> {
|
|
||||||
state: InType;
|
|
||||||
setState: (callback: SetStateFn<InType,OutType>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DeepError {
|
|
||||||
e: Error;
|
|
||||||
depth: number;
|
|
||||||
constructor(e, depth) {
|
|
||||||
this.e = e;
|
|
||||||
this.depth = depth;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResolvedType = Dynamic | DeepError | undefined;
|
|
||||||
|
|
||||||
export const serialize = (s: EditorState) => {
|
|
||||||
if (s.kind === "input") {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
// dirty: we write out the type in case the value is a literal and needs to be parsed
|
|
||||||
type: s.resolved && prettyT(s.resolved.t),
|
|
||||||
resolved: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (s.kind === "call") {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
fn: serialize(s.fn),
|
|
||||||
input: serialize(s.input),
|
|
||||||
resolved: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (s.kind === "let") {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
value: serialize(s.value),
|
|
||||||
inner: serialize(s.inner),
|
|
||||||
resolved: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (s.kind === "lambda") {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
expr: serialize(s.expr),
|
|
||||||
resolved: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseLiteral(text: string, type: string) {
|
|
||||||
// dirty
|
|
||||||
if (type === "Int") {
|
|
||||||
return {i: parseInt(text), t: Int};
|
|
||||||
}
|
|
||||||
if (type === "Double") {
|
|
||||||
return {i: parseDouble(text), t: Double};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deserialize = (s, env) => {
|
|
||||||
if (s.kind === "input") {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
resolved: trie.get(env.name2dyn)(s.text) || parseLiteral(s.text, s.type),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (s.kind === "call") {
|
|
||||||
const fn = deserialize(s.fn, env);
|
|
||||||
const input = deserialize(s.input, env);
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
fn,
|
|
||||||
input,
|
|
||||||
resolved: resolveCallBlock(fn.resolved, input.resolved),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (s.kind === "let") {
|
|
||||||
const value = deserialize(s.value, env);
|
|
||||||
const inner = deserialize(s.inner, makeInnerEnv(env, s.name, value.resolved));
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
value,
|
|
||||||
inner,
|
|
||||||
resolved: inner.resolved,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (s.kind === "lambda") {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
expr: deserialize(s.expr, env),
|
|
||||||
resolved: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { useEffect, useRef } from "react"
|
|
||||||
|
|
||||||
// like useEffect, but doesn't run on first render
|
|
||||||
|
|
||||||
export const useEffectBetter = (callback, deps) => {
|
|
||||||
// detect development mode, where render function is always called twice:
|
|
||||||
const firstRender = useRef(import.meta.env.MODE === "development" ? 2 : 1);
|
|
||||||
useEffect(() => {
|
|
||||||
if (firstRender.current > 0) {
|
|
||||||
firstRender.current -= 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}, deps);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue