dope2-webapp/src/CallBlock.tsx

214 lines
5.9 KiB
TypeScript

import { useState } from "react";
import { apply, UnifyError, assignFn, getType, getSymbol, symbolFunction } from "dope2";
import { Editor, type EditorState } from "./Editor";
import { Value } from "./Value";
import type { Dynamic, State2Props } from "./util/extra";
import "./CallBlock.css";
export interface CallBlockState<
FnState=EditorState,
InputState=EditorState,
> {
kind: "call";
env: any;
fn: FnState;
input: InputState;
resolved: undefined | Dynamic;
// focus: boolean;
}
interface CallBlockProps<
FnState=EditorState,
InputState=EditorState,
> extends State2Props<CallBlockState<FnState,InputState>> {
onResolve: (resolved: EditorState) => void;
}
function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
const [unifyError, setUnifyError] = useState<UnifyError | undefined>(undefined);
const {fn, input } = state;
const setFn = (fn: EditorState) => {
setState({...state, fn});
}
const setInput = (input: EditorState) => {
setState({...state, input});
}
const setResolved = (resolved?: Dynamic) => {
setState({...state, resolved});
}
const makeTheCall = (input, fn) => {
try {
const outputResolved = apply(input.resolved)(fn.resolved);
setResolved(outputResolved);
onResolve({
...state, resolved: outputResolved
});
setUnifyError(undefined);
}
catch (e) {
if (!(e instanceof UnifyError)) {
throw e;
}
setUnifyError(e);
onResolve({
...state, resolved: undefined
})
}
};
const onFnResolve = (fnState) => {
if (input.resolved) {
makeTheCall(input, fnState);
}
else {
// setFn(fnState);
setResolved(undefined);
onResolve({
...state, resolved: undefined
});
}
};
const onInputResolve = (inputState) => {
if (fn.resolved) {
makeTheCall(inputState, fn);
}
else {
setResolved(undefined);
onResolve({
...state, resolved: undefined
});
}
};
const onFnCancel = () => {
setState(input);
}
const onInputCancel = () => {
setState(fn);
}
return {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel};
}
export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
const {unifyError, setFn, setInput, onFnResolve, onInputResolve, onInputCancel}
= headlessCallBlock({ state, setState, onResolve });
return <span className={"functionBlock" + (unifyError ? " unifyError" : "")}>
<FunctionHeader
fn={state.fn}
setFn={setFn}
onFnResolve={onFnResolve}
input={state.input} />
<div className="functionParams">
<div className="outputParam">
{/* Sequence of input parameters */}
<InputParams
fn={state.fn} setFn={setFn}
input={state.input} setInput={setInput}
onFnResolve={onFnResolve}
onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
{/* Output (or Error) */}
{state.resolved && <Value dynamic={state.resolved} />}
{unifyError && unifyError.toString()}
</div>
</div>
</span>;
}
function FunctionHeader({ fn, setFn, input, onFnResolve }) {
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 {
onFnResolve : onFnFnResolve
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
return <FunctionHeader
fn={fn.fn}
setFn={fnFn => setFn({...fn, fn: fnFn})}
onFnResolve={onFnFnResolve}
input={fn.input} />;
}
else {
const filterCompatibleFns = ([_name, dynamic]: [string, Dynamic]) => {
if (input.resolved) {
try {
const type = getType(dynamic);
if (getSymbol(type) !== symbolFunction) {
return false;
}
assignFn(type, getType(input.resolved));
} catch (e) {
if (!(e instanceof UnifyError)) {
throw e;
}
return false;
}
}
return true;
}
// end of recursion - draw function name
return <div className="functionName">
&#119891;&#119899;&nbsp;
<Editor
state={fn}
setState={setFn}
onResolve={onFnResolve}
onCancel={() => {/*todo*/}}
filter={filterCompatibleFns} />
</div>;
}
}
function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel }) {
const filterCompatibleInputs = ([_name, dynamic]: [string, Dynamic]) => {
if (fn.resolved) {
try {
assignFn(getType(fn.resolved), getType(dynamic));
} catch (e) {
if (!(e instanceof UnifyError)) {
throw e;
}
return false;
}
}
return true;
}
return <div className="inputParam">
{(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
// Input(s) of the function we're calling:
<NestedInputParams fn={fn} setFn={setFn} onFnResolve={onFnResolve} />
}
{/* Our own input */}
<Editor
state={input}
setState={setInput}
onResolve={onInputResolve}
onCancel={onInputCancel}
filter={filterCompatibleInputs}
/>
</div>;
}
function NestedInputParams({fn, setFn, onFnResolve}) {
const {
onInputResolve: onFnInputResolve,
onFnResolve : onFnFnResolve,
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
return <InputParams
fn={fn.fn}
setFn={fnFn => setFn({...fn, fn: fnFn})}
input={fn.input}
setInput={fnInput => setFn({...fn, input: fnInput})}
onFnResolve={onFnFnResolve}
onInputResolve={onFnInputResolve}
onInputCancel={() => {/*todo*/}}
/>;
}