greatly simplify state + cleanup code

This commit is contained in:
Joeri Exelmans 2025-05-14 08:09:35 +02:00
parent 2d0deca127
commit 5964510036
11 changed files with 268 additions and 321 deletions

View file

@ -57,6 +57,6 @@ footer a {
} }
.factoryReset { .factoryReset {
background-color: red; background-color: rgb(255, 0, 0);
color: black; color: black;
} }

View file

@ -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) => {
setAppState(({history}) => {
const newState = callback(history.at(-1)!); const newState = callback(history.at(-1)!);
setHistory(history.concat([newState])); return {
setFuture([]); 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;
@ -94,15 +116,18 @@ export function App() {
})); }));
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => { const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
// @ts-ignore
if (e.target.value) {
// @ts-ignore // @ts-ignore
pushHistory(_ => examples[e.target.value][1]); 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}

View file

@ -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
} }
@ -79,7 +43,9 @@ 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} />
{/* &#x2611; */}
</>} </>}
</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">
&nbsp;&#119891;&#119899;&nbsp; &nbsp;&#119891;&#119899;&nbsp;
@ -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>;
} }

View file

@ -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>
: <></> : <></>
} }

View file

@ -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 && <>&#x2611;</>} */}
</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>
}</>; }</>;

View file

@ -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 {

View file

@ -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]);

View file

@ -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
View 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);
}

View file

@ -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,
}
}
};

View file

@ -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);
}