198 lines
No EOL
6 KiB
TypeScript
198 lines
No EOL
6 KiB
TypeScript
import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction, UnifyError } from "dope2";
|
|
|
|
import { Editor, type EditorState } from "./Editor";
|
|
import { Value } from "./Value";
|
|
import type { Dynamic, SetStateFn, State2Props } from "./util/extra";
|
|
|
|
import { useEffect } from "react";
|
|
import "./CallBlock.css";
|
|
|
|
type ResolvedType = Dynamic | Error | undefined;
|
|
|
|
export interface CallBlockState<
|
|
FnState=EditorState,
|
|
InputState=EditorState,
|
|
> {
|
|
kind: "call";
|
|
fn: FnState;
|
|
input: InputState;
|
|
resolved: ResolvedType;
|
|
}
|
|
|
|
interface CallBlockProps<
|
|
FnState=EditorState,
|
|
InputState=EditorState,
|
|
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
|
|
}
|
|
|
|
|
|
function have(resolved: ResolvedType) {
|
|
return resolved && !(resolved instanceof Error);
|
|
}
|
|
|
|
function headlessCallBlock({state, setState}: CallBlockProps) {
|
|
const {fn, input} = state;
|
|
const setFn = (callback: SetStateFn) => {
|
|
setState(state => ({...state, fn: callback(state.fn)}));
|
|
}
|
|
const setInput = (callback: SetStateFn) => {
|
|
setState(state => ({...state, input: callback(state.input)}));
|
|
}
|
|
const setResolved = (callback: SetStateFn<ResolvedType>) => {
|
|
setState(state => ({...state, resolved: callback(state.resolved)}));
|
|
}
|
|
useEffect(() => {
|
|
// 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(() => e as Error); // eval error
|
|
}
|
|
}
|
|
else if (input.resolved instanceof Error) {
|
|
setResolved(() => input.resolved); // bubble up the error
|
|
}
|
|
else if (fn.resolved instanceof Error) {
|
|
// @ts-ignore
|
|
setResolved(() => {
|
|
// @ts-ignore
|
|
return Object.assign(fn.resolved, {depth: (fn.resolved.depth || 0) + 1});
|
|
}); // bubble up the error
|
|
}
|
|
else {
|
|
// no errors and at least one is undefined:
|
|
setResolved(() => undefined); // chill out
|
|
}
|
|
}, [input.resolved, fn.resolved]);
|
|
const onFnCancel = () => {
|
|
setState(state => state.input); // we become our input
|
|
}
|
|
const onInputCancel = () => {
|
|
setState(state => state.fn); // we become our function
|
|
}
|
|
return {setFn, setInput, onFnCancel, onInputCancel};
|
|
}
|
|
|
|
export function CallBlock({ state, setState }: CallBlockProps) {
|
|
const {setFn, setInput, onFnCancel, onInputCancel}
|
|
= headlessCallBlock({ state, setState });
|
|
|
|
|
|
// @ts-ignore
|
|
console.log('depth:', state.resolved?.depth);
|
|
|
|
return <span className={"functionBlock" + ((state.resolved instanceof Error) ? " unifyError" : "")}>
|
|
<FunctionHeader
|
|
fn={state.fn}
|
|
setFn={setFn}
|
|
onFnCancel={onFnCancel}
|
|
input={state.input} />
|
|
<div className="functionParams">
|
|
<div className="outputParam">
|
|
{/* Sequence of input parameters */}
|
|
<InputParams
|
|
fn={state.fn} setFn={setFn}
|
|
input={state.input} setInput={setInput}
|
|
onInputCancel={onInputCancel}
|
|
depth={0}
|
|
// @ts-ignore
|
|
errorDepth={state.resolved instanceof Error ? (state.resolved.depth || 0) : -1}
|
|
/>
|
|
{/* Output (or Error) */}
|
|
{ state.resolved instanceof Error && state.resolved.toString()
|
|
|| state.resolved && <><Value dynamic={state.resolved} />☑</>}
|
|
</div>
|
|
</div>
|
|
</span>;
|
|
}
|
|
|
|
function filterFnInputs(fn: Dynamic|Error|undefined, input: Dynamic|Error|undefined) {
|
|
if (!have(fn) || !have(input)) {
|
|
return false;
|
|
}
|
|
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 }) {
|
|
if (fn.kind === "call") {
|
|
// if the function we're calling is itself the result of a function call,
|
|
// then we are anonymous, and so we don't draw a function name
|
|
|
|
// recurse:
|
|
const {
|
|
setFn : setFnFn,
|
|
onFnCancel : onFnFnCancel,
|
|
} = headlessCallBlock({state: fn, setState: setFn});
|
|
|
|
return <FunctionHeader
|
|
fn={fn.fn}
|
|
setFn={setFnFn}
|
|
onFnCancel={onFnFnCancel}
|
|
input={fn.input} />;
|
|
}
|
|
else {
|
|
// end of recursion - draw function name
|
|
return <span className="functionName">
|
|
𝑓𝑛
|
|
<Editor
|
|
state={fn}
|
|
setState={setFn}
|
|
onCancel={onFnCancel}
|
|
filter={([_, fnCandidate]) => filterFnInputs(fnCandidate, input.resolved)} />
|
|
</span>;
|
|
}
|
|
}
|
|
|
|
function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth }) {
|
|
return <div className={"inputParam" + (depth === errorDepth ? " offending" : "")}>
|
|
{(fn.kind === "call") &&
|
|
// if the function we're calling is itself the result of a function call,
|
|
// then we render its input parameter nested in our own input parameter box, which is way more readable
|
|
|
|
// recurse:
|
|
<NestedParams fn={fn} setFn={setFn} depth={depth} errorDepth={errorDepth}/>
|
|
}
|
|
{/* Our own input */}
|
|
<Editor
|
|
state={input}
|
|
setState={setInput}
|
|
onCancel={onInputCancel}
|
|
filter={([_, inputCandidate]) => filterFnInputs(fn.resolved, inputCandidate)}
|
|
/>
|
|
</div>;
|
|
}
|
|
|
|
function NestedParams({fn, setFn, depth, errorDepth}) {
|
|
const {
|
|
setFn : setFnFn,
|
|
setInput : setFnInput,
|
|
} = headlessCallBlock({state: fn, setState: setFn});
|
|
return <InputParams
|
|
fn={fn.fn}
|
|
setFn={setFnFn}
|
|
input={fn.input}
|
|
setInput={setFnInput}
|
|
onInputCancel={() => {/*todo*/}}
|
|
depth={depth+1}
|
|
errorDepth={errorDepth}
|
|
/>;
|
|
} |