From 2d0deca1270c1b16dd62b24dfd63fb51c09874b6 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 14 May 2025 06:46:03 +0200 Subject: [PATCH] don't re-compute values on first render (unnecessary, values are part of state) --- src/App.css | 5 ++ src/App.tsx | 66 +++++++++++++++++---- src/CallBlock.css | 1 + src/CallBlock.tsx | 70 ++++++++++------------ src/Editor.tsx | 4 +- src/InputBlock.tsx | 6 +- src/LambdaBlock.tsx | 5 +- src/LetInBlock.tsx | 28 ++++++--- src/Value.tsx | 62 +++++++++---------- src/configurations.ts | 1 - src/types.ts | 108 ++++++++++++++++++++++++++++++++++ src/util/dom_trickery.ts | 4 +- src/util/extra.ts | 13 ---- src/util/use_effect_better.ts | 16 +++++ 14 files changed, 274 insertions(+), 115 deletions(-) create mode 100644 src/types.ts delete mode 100644 src/util/extra.ts create mode 100644 src/util/use_effect_better.ts diff --git a/src/App.css b/src/App.css index e48e754..75075ee 100644 --- a/src/App.css +++ b/src/App.css @@ -55,3 +55,8 @@ footer a { background-color: dodgerblue; color: white; } + +.factoryReset { + background-color: red; + color: black; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 05505d0..3b8be84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,47 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import './App.css' import { Editor, type EditorState } from './Editor' import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations"; import { CommandContext } from './CommandContext'; -import { EnvContext } from './EnvContext'; +import { deserialize, serialize } from './types'; +import { extendedEnv } from './EnvContext'; +import { useEffectBetter } from './util/use_effect_better'; + +const commands: [string, string[], string][] = [ + ["call" , ['c' ], "call" ], + ["eval" , ['e','Tab','Enter'], "eval" ], + ["transform", ['t', '.' ], "transform" ], + ["let" , ['l', '=', 'a' ], "let ... in ..."], +]; + +const examples: [string, EditorState][] = [ + ["empty editor", initialEditorState], + ["push to list", nonEmptyEditorState], + ["function w/ 4 params", tripleFunctionCallEditorState]]; export function App() { // const [history, setHistory] = useState([initialEditorState]); // const [history, setHistory] = useState([nonEmptyEditorState]); - const [history, setHistory] = useState([tripleFunctionCallEditorState]); + // const [history, setHistory] = useState([tripleFunctionCallEditorState]); + // const [future, setFuture] = useState([]); - const [future, setFuture] = useState([]); + // load from localStorage + const [history, setHistory] = useState( + localStorage["history"] + ? JSON.parse(localStorage["history"]).map(s => deserialize(s, extendedEnv)) + : [initialEditorState] + ); + const [future, setFuture] = useState( + localStorage["future"] + ? JSON.parse(localStorage["future"]).map(s => deserialize(s, extendedEnv)) + : [] + ); + + useEffectBetter(() => { + // persist accross reloads + localStorage["history"] = JSON.stringify(history.map(serialize)); + localStorage["future"] = JSON.stringify(future.map(serialize)); + }, [history, future]); const pushHistory = (callback: (p: EditorState) => EditorState) => { const newState = callback(history.at(-1)!); @@ -51,12 +82,6 @@ export function App() { window.onkeydown = onKeyDown; }, []); - const commands: [string, string[], string][] = [ - ["call" , ['c' ], "call" ], - ["eval" , ['e','Tab','Enter' ], "eval" ], - ["transform", ['t', '.' ], "transform" ], - ["let" , ['l', '=', 'a' ], "let ... in ..."], - ]; const [highlighted, setHighlighted] = useState( commands.map(() => false)); @@ -68,12 +93,16 @@ export function App() { }]; })); + const onSelectExample = (e: React.SyntheticEvent) => { + // @ts-ignore + pushHistory(_ => examples[e.target.value][1]); + } + return ( <>
- - - Commands: + + { commands.map(([_, keys, descr], i) => @@ -81,6 +110,17 @@ export function App() { {descr} ) } + +
diff --git a/src/CallBlock.css b/src/CallBlock.css index 76d51bd..63ae30a 100644 --- a/src/CallBlock.css +++ b/src/CallBlock.css @@ -3,6 +3,7 @@ display: inline-block; margin: 4px; color: black; + background-color: white; } .functionName { diff --git a/src/CallBlock.tsx b/src/CallBlock.tsx index a038e6f..09da3b4 100644 --- a/src/CallBlock.tsx +++ b/src/CallBlock.tsx @@ -2,21 +2,11 @@ import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction, import { Editor, type EditorState } from "./Editor"; import { Value } from "./Value"; -import type { Dynamic, SetStateFn, State2Props } from "./util/extra"; +import { DeepError, type ResolvedType, type SetStateFn, type State2Props } from "./types"; import { useEffect } from "react"; import "./CallBlock.css"; - -export class DeepError { - e: Error; - depth: number; - constructor(e, depth) { - this.e = e; - this.depth = depth; - } -}; - -type ResolvedType = Dynamic | DeepError | undefined; +import { useEffectBetter } from "./util/use_effect_better"; export interface CallBlockState< FnState=EditorState, @@ -34,6 +24,26 @@ interface CallBlockProps< > extends State2Props,EditorState> { } +export function resolveCallBlock(fn: ResolvedType, input: ResolvedType) { + 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); @@ -50,35 +60,13 @@ function headlessCallBlock({state, setState}: CallBlockProps) { const setResolved = (callback: SetStateFn) => { setState(state => ({...state, resolved: callback(state.resolved)})); } - useEffect(() => { + + 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. - if (have(input.resolved) && have(fn.resolved)) { - try { - const outputResolved = apply(input.resolved)(fn.resolved); // may throw - setResolved(() => outputResolved); // success - } - catch (e) { - if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) { - throw e; - } - setResolved(() => new DeepError(e, 0)); // eval error - } - } - else if (input.resolved instanceof DeepError) { - setResolved(() => input.resolved); // bubble up the error - } - else if (fn.resolved instanceof DeepError) { - setResolved(() => { - // @ts-ignore - return new DeepError(fn.resolved.e, fn.resolved.depth+1); - }); // bubble up the error - } - else { - // no errors and at least one is undefined: - setResolved(() => undefined); // chill out - } - }, [input.resolved, fn.resolved]); + setResolved(() => resolveCallBlock(fn.resolved, input.resolved)); + }, [fn.resolved, input.resolved]); + const onFnCancel = () => { setState(state => state.input); // we become our input } @@ -109,7 +97,9 @@ export function CallBlock({ state, setState }: CallBlockProps) { /> {/* Output (or Error) */} { state.resolved instanceof DeepError && state.resolved.e.toString() - || state.resolved && <>☑} + || state.resolved && <> + {/* ☑ */} + } ; diff --git a/src/Editor.tsx b/src/Editor.tsx index 4dc4bd5..beafac5 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -1,10 +1,10 @@ import { getSymbol, getType, symbolFunction } from "dope2"; import { useContext, useEffect, useReducer, useRef, useState } from "react"; -import { CallBlock, DeepError, type CallBlockState } from "./CallBlock"; +import { CallBlock, type CallBlockState } from "./CallBlock"; import { InputBlock, type InputBlockState } from "./InputBlock"; import { Type } from "./Type"; -import { type Dynamic, type State2Props } from "./util/extra"; +import { DeepError, type Dynamic, type State2Props } from "./types"; import "./Editor.css"; import { LetInBlock, type LetInBlockState } from "./LetInBlock"; diff --git a/src/InputBlock.tsx b/src/InputBlock.tsx index dc3f882..6cdd29b 100644 --- a/src/InputBlock.tsx +++ b/src/InputBlock.tsx @@ -6,7 +6,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { Type } from "./Type"; import "./InputBlock.css"; -import type { Dynamic, State2Props } from "./util/extra"; +import type { Dynamic, State2Props } from "./types"; import { EnvContext } from "./EnvContext"; export interface InputBlockState { @@ -30,7 +30,7 @@ const computeSuggestions = (text, env, filter) => { ... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []), ... trie.suggest(env.name2dyn)(text)(Infinity), ] - return ls; + // return ls; return [ ...ls.filter(filter), // ones that match filter come first ...ls.filter(s => !filter(s)), @@ -158,7 +158,7 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp spellCheck={false}/> {/* Single 'grey' suggestion */} {singleSuggestion} - { resolved && <>☑} + {/* { resolved && <>☑} */} ; } diff --git a/src/LambdaBlock.tsx b/src/LambdaBlock.tsx index e255de8..2b5e2f2 100644 --- a/src/LambdaBlock.tsx +++ b/src/LambdaBlock.tsx @@ -1,11 +1,10 @@ import type { EditorState } from "./Editor"; -import type { Dynamic } from "./util/extra"; +import type { Dynamic, ResolvedType } from "./types"; export interface LambdaBlockState { kind: "lambda"; - env: any; paramName: string; expr: EditorState; - resolved: undefined | Dynamic; + resolved: ResolvedType; } diff --git a/src/LetInBlock.tsx b/src/LetInBlock.tsx index 3d5733a..63cb79f 100644 --- a/src/LetInBlock.tsx +++ b/src/LetInBlock.tsx @@ -1,31 +1,39 @@ import { useContext, useEffect, useRef } from "react"; import { Editor, type EditorState } from "./Editor"; import { EnvContext } from "./EnvContext"; -import type { Dynamic, State2Props } from "./util/extra"; +import { DeepError, type ResolvedType, type State2Props } from "./types"; import { growEnv } from "dope2"; import { autoInputWidth } from "./util/dom_trickery"; import "./LetInBlock.css"; +import { useEffectBetter } from "./util/use_effect_better"; export interface LetInBlockState { kind: "let"; name: string; value: EditorState; inner: EditorState; - resolved: undefined | Dynamic; + resolved: ResolvedType; } interface LetInBlockProps extends State2Props { } +export function makeInnerEnv(env, name: string, value: ResolvedType) { + if (value && !(value instanceof DeepError)) { + return growEnv(env)(name)(value) + } + return env; +} export function LetInBlock({state, setState}: LetInBlockProps) { const {name, value, inner} = state; const env = useContext(EnvContext); + const innerEnv = makeInnerEnv(env, name, value.resolved); const nameRef = useRef(null); - const setInner = inner => setState(state => ({...state, inner})); - const setValue = value => setState(state => ({...state, value})); + const setInner = callback => setState(state => ({...state, inner: callback(state.inner)})); + const setValue = callback => setState(state => ({...state, value: callback(state.value)})); const onChangeName = (e: React.ChangeEvent) => { setState(state => ({...state, name: e.target.value})); @@ -35,10 +43,14 @@ export function LetInBlock({state, setState}: LetInBlockProps) { nameRef.current?.focus(); }, []); - useEffect(() => autoInputWidth(nameRef, name), [nameRef, name]); + useEffectBetter(() => { + // bubble up + setState(state => ({...state, resolved: inner.resolved})); + }, [inner.resolved]) + + useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]); + - const innerEnv = (name !== '') && value.resolved - && growEnv(env)(name)(value.resolved) || env; return
let @@ -46,7 +58,7 @@ export function LetInBlock({state, setState}: LetInBlockProps) { ref={nameRef} className='editable' value={name} - placeholder="" + placeholder="" onChange={onChangeName} /> = diff --git a/src/Value.tsx b/src/Value.tsx index b9e0be0..28a789f 100644 --- a/src/Value.tsx +++ b/src/Value.tsx @@ -1,4 +1,4 @@ -import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool} from "dope2"; +import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set} from "dope2"; import "./Value.css"; @@ -19,21 +19,14 @@ export function Value({dynamic}) { switch (symbol) { case symbolFunction: return ; - // return ; - // case symbolProduct: - // return ; case symbolSum: return ; - case symbolProduct: return ; - case symbolDict: return ; - - // return ; - // case symbolSet: - // return ; + case symbolSet: + return ; case symbolList: return ; @@ -60,6 +53,15 @@ function ValueBool({val}) { function ValueList({val, elemType}) { return [{val.map((v, i) => )}]; } +function ValueSet({val, elemType}) { + return {'{'}{set.fold(acc => elem => acc.concat([elem]))([])(val).map((v, i) => )}{'}'}; +} +function ValueDict({val, keyType, valueType}) { + return {'{'}{set.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => + + + )}{'}'}; +} function ValueSum({val, leftType, rightType}) { return match(val) (l => L ) @@ -68,23 +70,23 @@ function ValueSum({val, leftType, rightType}) { function ValueProduct({val, leftType, rightType}) { return (); } -function ValueDict({val, keyType, valueType}) { - let i=0; - return {'{'}<>{ - dict.fold - (acc => key => value => { - console.log({acc, key, value}); - return acc.concat([<> - - ⇒ - - ]); - }) - ([]) - (val) - .map(result => { - console.log(result); - return result; - }) - }{'}'}; -} \ No newline at end of file +// function ValueDict({val, keyType, valueType}) { +// let i=0; +// return {'{'}<>{ +// dict.fold +// (acc => key => value => { +// console.log({acc, key, value}); +// return acc.concat([<> +// +// ⇒ +// +// ]); +// }) +// ([]) +// (val) +// .map(result => { +// console.log(result); +// return result; +// }) +// }{'}'}; +// } diff --git a/src/configurations.ts b/src/configurations.ts index d6e39a8..aee1221 100644 --- a/src/configurations.ts +++ b/src/configurations.ts @@ -93,4 +93,3 @@ export const tripleFunctionCallEditorState: EditorState = { }, resolved: apply(fourtyFive)(apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith4Params)))), }; - diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..e5809dc --- /dev/null +++ b/src/types.ts @@ -0,0 +1,108 @@ +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 = (state: InType) => OutType; + +export interface State2Props { + state: InType; + setState: (callback: SetStateFn) => 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, + } + } +}; diff --git a/src/util/dom_trickery.ts b/src/util/dom_trickery.ts index 915b145..3bbc909 100644 --- a/src/util/dom_trickery.ts +++ b/src/util/dom_trickery.ts @@ -49,8 +49,8 @@ export function focusPrevElement() { } } -export const autoInputWidth = (inputRef: React.RefObject, text) => { +export const autoInputWidth = (inputRef: React.RefObject, text, emptyWidth=150) => { if (inputRef.current) { - inputRef.current.style.width = `${text.length === 0 ? 150 : (text.length*8.7)}px`; + inputRef.current.style.width = `${text.length === 0 ? emptyWidth : (text.length*8.7)}px`; } } diff --git a/src/util/extra.ts b/src/util/extra.ts deleted file mode 100644 index a00e315..0000000 --- a/src/util/extra.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { EditorState } from "../Editor"; - -export interface Dynamic { - i: any; - t: any; -} - -export type SetStateFn = (state: InType) => OutType; - -export interface State2Props { - state: InType; - setState: (callback: SetStateFn) => void; -} diff --git a/src/util/use_effect_better.ts b/src/util/use_effect_better.ts new file mode 100644 index 0000000..4230b61 --- /dev/null +++ b/src/util/use_effect_better.ts @@ -0,0 +1,16 @@ +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); +}