flow: expressions

This commit is contained in:
cha0s 2020-06-23 19:45:38 -05:00
parent 7a0915cd56
commit 234421b674
13 changed files with 146 additions and 230 deletions

View File

@ -38,6 +38,7 @@
"html-entities": "1.3.1",
"immer": "^7.0.1",
"lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"memorystore": "^1.6.2",
"normalizr": "^3.6.0",
"prop-types": "^15",

View File

@ -11,6 +11,6 @@
text-align-last: center;
}
.operand .traversal {
.operand .expression {
display: flex;
}

View File

@ -1,3 +1,4 @@
import {makeCompilable} from '@avocado/behavior';
import {compose} from '@avocado/core';
import contempo from 'contempo';
import PropTypes from 'prop-types';
@ -6,7 +7,6 @@ import React, {useRef} from 'react';
import Value from '~/client/value';
import propTypes from './prop-types';
import {toBehaviorItem} from './typing';
const binaryOps = [
['is', 'is equal to'],
@ -36,7 +36,7 @@ const Condition = ({
const {operands} = value;
const mergeToOperand = (index, operandValue) => [
...operands.slice(0, index),
toBehaviorItem(operandValue),
makeCompilable(operandValue),
...operands.slice(index + 1),
];
const makeOnChange = (index) => (newValue, event) => {
@ -44,6 +44,7 @@ const Condition = ({
operatorRef.current === event.target
? {operator: newValue}
: {operands: mergeToOperand(index, newValue)},
event,
);
};
return (

View File

@ -34,10 +34,10 @@
}
.arg label {
padding: 0.5em;
padding: 0;
}
.arg .traversal {
.arg .expression {
display: flex;
}
@ -58,5 +58,5 @@
.op {
align-self: flex-start;
margin: 0.5em 1em;
margin: 0.125em;
}

View File

@ -1,3 +1,4 @@
import {makeCompilable} from '@avocado/behavior';
import {compose} from '@avocado/core';
import contempo from 'contempo';
import React from 'react';
@ -6,15 +7,13 @@ import Value from '~/client/value';
import propTypes from './prop-types';
import {
defaultSteps,
descriptionFromSteps,
stepsOptions,
toBehaviorItem,
typeFromSteps,
defaultOps,
descriptionFromOps,
opsOptions,
} from './typing';
const decorate = compose(
contempo(require('./traversal.raw.scss')),
contempo(require('./expression.raw.scss')),
);
const Traversal = (props) => {
@ -24,37 +23,36 @@ const Traversal = (props) => {
type,
value = {},
} = props;
const {steps, value: assignValue} = value;
const stepsType = type || typeFromSteps(context, steps);
const optionsList = stepsOptions(context, steps, stepsType);
const {ops, assign} = value;
const optionsList = opsOptions(context, ops, type);
return (
<div className="traversal">
{steps.map((step, i) => (
<div className="expression">
{ops.map((op, i) => (
// eslint-disable-next-line react/no-array-index-key
<span className="step" key={i}>
<span className="op" key={i}>
{
(() => {
switch (step.type) {
switch (op.type) {
case 'key': {
const options = optionsList[i];
return (
<span className="key" data-step-key={step.key}>
<span className="key" data-op-key={op.key}>
<select
onChange={(event) => {
const newSteps = [
...steps.slice(0, i),
...ops.slice(0, i),
{
...steps[i],
...ops[i],
key: event.target.value,
},
];
return onChange({
...value,
steps: defaultSteps(context, stepsType, newSteps),
ops: defaultOps(context, type, newSteps),
value: undefined,
});
}, event);
}}
value={step.key}
value={op.key}
>
{
options
@ -66,12 +64,12 @@ const Traversal = (props) => {
);
}
case 'invoke': {
const description = descriptionFromSteps(context, steps);
const description = descriptionFromOps(context, ops.slice(0, i + 1));
const argTypes = (description.args || []).map(([, {type: argType}]) => argType);
return (
<div className="invoke">
<span className="paren open">(</span>
{step.args.map((arg, j) => (
{op.args.map((arg, j) => (
<div className="arg" key={JSON.stringify(arg)}>
<label>
{arg.label}
@ -79,20 +77,20 @@ const Traversal = (props) => {
context={context}
onChange={(argValue, event) => (
onChange({
type: 'traversal',
steps: [
...steps.slice(0, i),
type: 'expression',
ops: [
...ops.slice(0, i),
{
...steps[i],
...ops[i],
args: [
...steps[i].args.slice(0, j),
toBehaviorItem(argValue),
...steps[i].args.slice(j + 1),
...ops[i].args.slice(0, j),
makeCompilable(argValue),
...ops[i].args.slice(j + 1),
],
},
...steps.slice(i + 1),
...ops.slice(i + 1),
],
value: assignValue,
assign,
}, event)
)}
type={argTypes[j] || 'any'}
@ -113,20 +111,20 @@ const Traversal = (props) => {
</span>
))}
{
assignValue && (
assign && (
<span className="assign">
<span className="op">=</span>
<Value.Component
context={context}
onChange={(valueValue, event) => (
onChange({
type: 'traversal',
steps,
value: toBehaviorItem(valueValue),
type: 'expression',
ops,
value: makeCompilable(valueValue),
}, event)
)}
type={typeFromSteps(context, steps)}
value={assignValue}
type={type}
value={assign}
/>
</span>
)
@ -140,6 +138,6 @@ Traversal.propTypes = {
};
export default {
type: 'traversal',
type: 'expression',
Component: decorate(Traversal),
};

View File

@ -4,44 +4,44 @@ import PropTypes from 'prop-types';
import React from 'react';
import propTypes from './prop-types';
import Traversal from './traversal.type-renderer';
import Expression from './expression.type-renderer';
const decorate = compose(
contempo(require('./actions.raw.scss')),
contempo(require('./expressions.raw.scss')),
);
const Actions = ({
const Expressions = ({
context,
onChange = () => {},
value = {},
}) => {
const {traversals = []} = value;
const {expressions = []} = value;
return (
<div className="actions">
<div className="expressions">
<ol>
{
traversals.length > 0 && (
traversals.map(
(traversal, i) => (
expressions.length > 0 && (
expressions.map(
(expression, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={i}>
<Traversal.Component
<Expression.Component
context={context}
onChange={(traversalValue, event) => (
onChange={(expressionValue, event) => (
onChange({
...value,
traversals: [
...traversals.slice(0, i),
expressions: [
...expressions.slice(0, i),
{
...traversals[i],
...traversalValue,
...expressions[i],
...expressionValue,
},
...traversals.slice(i + 1),
...expressions.slice(i + 1),
],
}, event)
)}
type="any"
value={traversal}
value={expression}
/>
</li>
),
@ -53,12 +53,12 @@ const Actions = ({
);
};
Actions.propTypes = {
Expressions.propTypes = {
...propTypes,
value: PropTypes.shape({}).isRequired,
};
export default {
type: 'actions',
Component: decorate(Actions),
type: 'expressions',
Component: decorate(Expressions),
};

View File

@ -5,7 +5,7 @@ import React from 'react';
import useTypeRenderers from '~/client/hooks/useTypeRenderers';
import propTypes from './prop-types';
import {defaultSteps, stepsOptions, typeFromLiteral} from './typing';
import {defaultOps, opsOptions} from './typing';
const decorate = compose(
contempo(require('./literal.raw.scss')),
@ -14,12 +14,12 @@ const decorate = compose(
const Literal = ({
context,
onChange = () => {},
type,
value,
}) => {
const typeRenderers = useTypeRenderers();
const type = typeFromLiteral(value.value);
const Component = typeRenderers[type];
const [options] = stepsOptions(context, [], type);
const [options] = opsOptions(context, [], type);
options.push('<literal>');
return (
<div className="literal">
@ -27,9 +27,9 @@ const Literal = ({
onChange={(event) => {
if ('<literal>' !== event.target.value) {
onChange({
type: 'traversal',
steps: defaultSteps(context, type, [{type: 'key', key: event.target.value}]),
});
type: 'expression',
ops: defaultOps(context, type, [{type: 'key', key: event.target.value}]),
}, event);
}
}}
value="<literal>"
@ -40,7 +40,7 @@ const Literal = ({
.map((option) => <option key={option}>{option}</option>)
}
</select>
{Component ? <Component onChange={onChange} value={value.value} /> : null}
{Component ? <Component onChange={onChange} type={type} value={value.value} /> : null}
</div>
);
};

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import propTypes from './prop-types';
import Actions from './actions.type-renderer';
import Expressions from './expressions.type-renderer';
const decorate = compose(
contempo(require('./routines.raw.scss')),
@ -15,8 +15,7 @@ const Routines = ({
onChange = () => {},
value = {},
}) => {
const {routines = {}} = value;
const entries = Object.entries(routines);
const entries = Object.entries(value);
return (
<div className="routines">
<ol>
@ -27,23 +26,15 @@ const Routines = ({
<li className="routine" key={name}>
<label>
<span className="text">{name}</span>
<Actions.Component
<Expressions.Component
context={context}
onChange={(actionsValue, event) => (
onChange={(expressions, event) => (
onChange({
...value,
routines: {
...routines,
[name]: {
...routine,
routine: {
...actionsValue,
},
},
},
[name]: expressions,
}, event)
)}
value={routine.routine}
value={routine}
/>
</label>
</li>

View File

@ -1,138 +1,63 @@
import {Context} from '@avocado/behavior';
import {Traversal} from '@avocado/behavior/item/traversal';
import {
candidates as typeCandidates,
compile,
description as typeDescription,
fitsInto,
} from '@avocado/behavior';
let digraph;
// eslint-disable-next-line no-return-assign
const cachedDigraph = () => (digraph || (digraph = Context.allTypesInvertedDigraph()));
export function isBehaviorItem(valueOrItem) {
if ('object' !== typeof valueOrItem) {
return false;
}
return -1 !== [
'actions',
'literal',
'routines',
'traversal',
].indexOf(valueOrItem.type);
}
export function toBehaviorItem(valueOrItem) {
return isBehaviorItem(valueOrItem) ? valueOrItem : {type: 'literal', value: valueOrItem};
}
export function typeFromLiteral(value) {
if ('undefined' === typeof value) {
return 'undefined';
}
if (null === value) {
return 'null';
}
if ('number' === typeof value) {
return 'number';
}
if ('string' === typeof value) {
return 'string';
}
if ('boolean' === typeof value) {
return 'bool';
}
if (value.length && 2 === value.length && value instanceof Array) {
return 'vector';
}
return 'object';
}
const fakeContextDescription = (context) => {
const children = Object.entries(context.all())
.reduce((r, [key, tuple]) => ({...r, [key]: {type: tuple[1]}}), {});
return {children};
};
const stepsValue = (context, steps) => (
steps.length > 0 ? (new Traversal({steps})).get(context) : undefined
);
export function descriptionFromSteps(context, steps) {
let description = fakeContextDescription(context);
if (0 === steps.length) {
export function descriptionFromOps(context, ops) {
let description = context.description();
if (0 === ops.length) {
return description;
}
const isInvocation = 'invoke' === steps[steps.length - 1].type;
const keyStepsCount = steps.length - (isInvocation ? 1 : 0);
let variable;
for (let i = 0; i < keyStepsCount; ++i) {
const {children = {}} = description;
const {key} = steps[i];
variable = stepsValue(context, steps.slice(0, i + 1));
for (let i = 0; i < ops.length; ++i) {
if ('invoke' === ops[i].type) {
continue;
}
const {children} = description;
const {key} = ops[i];
const evaluate = compile({type: 'expression', ops: ops.slice(0, i + 1)});
variable = evaluate(context);
if ('function' === typeof variable && children[key] && children[key].args) {
description = children[key];
const {type} = children[key];
description = {
...typeDescription(type),
...children[key],
};
}
else {
const candidateDescription = Context.typeDescription(
description = typeDescription(
children[key].type,
'function' !== typeof variable || (children[key] && 'function' === children[key].type)
'function' !== typeof variable || 'function' === children[key].type
? variable
: undefined,
);
description = 0 !== Object.keys(candidateDescription.children || {}).length
? candidateDescription
: children[key];
}
}
return description;
}
export function typeFits(reference, candidate) {
return 'any' === reference || -1 !== (reference.split('|') || []).indexOf(candidate);
}
const descriptionCandidates = (description, type) => {
const inverted = cachedDigraph();
const candidates = (inverted[type] || []).concat(type);
const {children = {}} = description;
return 'any' === type
? Object.keys(children)
: Object.entries(children)
.reduce((r, [key, spec]) => (
candidates.find((candidate) => typeFits(candidate, spec.type))
? r.concat(key)
: r
), []);
};
export function stepsOptions(context, steps, type) {
const optionsList = [
descriptionCandidates(
fakeContextDescription(context),
type,
),
];
if (0 === steps.length) {
export function opsOptions(context, ops, type) {
const optionsList = [typeCandidates(context.description(), type)];
if (0 === ops.length) {
return optionsList;
}
const isInvocation = 'invoke' === steps[steps.length - 1].type;
const keyStepsCount = steps.length - (isInvocation ? 1 : 0);
for (let i = 1; i < keyStepsCount; ++i) {
optionsList.push(descriptionCandidates(
descriptionFromSteps(context, steps.slice(0, i)),
type,
));
const isInvocation = 'invoke' === ops[ops.length - 1].type;
const keyOpsCount = ops.length - (isInvocation ? 1 : 0);
for (let i = 1; i < keyOpsCount; ++i) {
optionsList.push(typeCandidates(descriptionFromOps(context, ops.slice(0, i)), type));
}
return optionsList;
}
export function typeFromSteps(context, steps) {
return 0 === steps.length ? 'undefined' : descriptionFromSteps(context, steps).type;
}
const defaultInvocation = (context, steps) => {
const funcDescription = descriptionFromSteps(context, steps);
const defaultInvocation = (context, ops) => {
const funcDescription = descriptionFromOps(context, ops);
const {args = []} = funcDescription;
return {
type: 'invoke',
args: args.map(([, {type}]) => {
const {defaultLiteral} = Context.typeDescription(type, undefined);
const {defaultLiteral} = typeDescription(type, undefined);
if ('undefined' !== defaultLiteral) {
return ({
type: 'literal',
@ -140,53 +65,56 @@ const defaultInvocation = (context, steps) => {
});
}
return ({
type: 'traversal',
type: 'expression',
// eslint-disable-next-line no-use-before-define
steps: defaultSteps(context, type),
ops: defaultOps(context, type),
});
}),
};
};
export const defaultSteps = (context, type, originalSteps = []) => {
const steps = [...originalSteps];
let stepsType = typeFromSteps(context, originalSteps);
const typesVisited = {[stepsType]: true};
while (!typeFits(type, stepsType) || 0 === steps.length) {
if ('function' === typeof stepsValue(context, steps)) {
export const defaultOps = (context, type, originalOps = []) => {
const ops = [...originalOps];
let evaluated;
let {type: opsType} = descriptionFromOps(context, originalOps);
const typesVisited = {[opsType]: true};
while (!fitsInto(opsType, type) || 0 === ops.length) {
evaluated = compile({type: 'expression', ops})(context);
if (
'invoke' !== ops[ops.length - 1].type
&& 'function' === typeof evaluated
) {
if ('function' === type) {
return steps;
return ops;
}
steps.push(defaultInvocation(context, steps));
ops.push(defaultInvocation(context, ops));
}
else {
const description = descriptionFromSteps(context, steps);
const description = descriptionFromOps(context, ops);
const {children = {}} = description;
const candidates = descriptionCandidates(description, type);
const candidates = typeCandidates(description, type);
const key = candidates.find((candidate) => {
const {type: candidateType} = children[candidate];
if (typesVisited[candidateType]) {
return false;
}
if (typeFits(type, candidateType)) {
if (fitsInto(type, candidateType)) {
return true;
}
typesVisited[candidateType] = true;
return true;
});
steps.push({type: 'key', key});
if ('function' === typeof stepsValue(context, steps)) {
steps.push(defaultInvocation(context, steps));
ops.push({type: 'key', key});
evaluated = compile({type: 'expression', ops})(context);
if ('function' === typeof evaluated) {
ops.push(defaultInvocation(context, ops));
}
}
stepsType = typeFromSteps(context, steps);
opsType = descriptionFromOps(context, ops).type;
}
if ('function' === typeof stepsValue(context, steps)) {
steps.push(defaultInvocation(context, steps));
evaluated = compile({type: 'expression', ops})(context);
if ('function' === typeof evaluated) {
ops.push(defaultInvocation(context, ops));
}
return steps;
return ops;
};
export function typeFromValue(v) {
return isBehaviorItem(v) ? v.type : undefined;
}

View File

@ -16,12 +16,12 @@ const Vector = ({
}) => (
<span className="vector">
<Number.Component
onChange={(axe) => onChange([axe, value[1]])}
onChange={(axe, event) => onChange([axe, value[1]], event)}
value={value[0]}
/>
<span className="separator">{' x '}</span>
<Number.Component
onChange={(axe) => onChange([value[0], axe])}
onChange={(axe, event) => onChange([value[0], axe], event)}
value={value[1]}
/>
</span>

View File

@ -1,10 +1,10 @@
import {isCompilable} from '@avocado/behavior';
import {compose} from '@avocado/core';
import contempo from 'contempo';
import PropTypes from 'prop-types';
import React from 'react';
import useTypeRenderers from '~/client/hooks/useTypeRenderers';
import {typeFromLiteral, typeFromValue} from '~/client/types/typing';
import propTypes from './types/prop-types';
@ -16,19 +16,11 @@ const Value = ({
context,
onChange = () => {},
options,
type: typeHint,
type,
value,
}) => {
const typeRenderers = useTypeRenderers();
const fromValue = typeFromValue(value);
let type;
if (typeHint) {
type = typeHint;
}
else {
type = 'undefined' !== typeof fromValue ? fromValue : typeFromLiteral(value);
}
const Component = typeRenderers[fromValue || type];
const Component = typeRenderers[isCompilable(value) ? value.type : type];
return (
<span className="value">
{

View File

@ -5929,6 +5929,11 @@ lodash.mapvalues@4.6.0:
resolved "https://npm.i12e.cha0s.io/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://npm.i12e.cha0s.io/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.mergewith@4.6.1:
version "4.6.1"
resolved "https://npm.i12e.cha0s.io/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"