mson-three/src/mson/eval.ts

306 lines
8.0 KiB
TypeScript

import { deepClone } from '../util/deep-clone';
import { isObject } from '../util/merge-deep';
import {
MsonComponent,
MsonData,
MsonEvaluatedLocals,
MsonEvaluatedModel,
MsonExpression,
MsonLocals,
MsonSlot,
MsonVariable,
} from './mson.type';
import { ModelStore } from './store';
type MsonVariables = Record<string, number>;
/**
* This class is used to evaluate all variables within a MSON model definition.
*/
export class MsonEvaluate {
constructor(protected readonly store: ModelStore) {}
/**
* Evaluate a model, resolving all of its references and parameters.
* @param model MSON data to evaluate
* @param useLocals Override locals
* @returns Evaluated model, with (hopefully) all variables replaced.
*/
evaluateModel(
model: string,
useLocals?: MsonEvaluatedLocals,
): MsonEvaluatedModel {
const modelData = this.store.getModelByName(model);
if (!modelData) {
throw new Error(`Model ${model} not found for evaluation`);
}
// Deep copy the model
const localCopy = deepClone(modelData) as MsonEvaluatedModel;
localCopy.name = model;
localCopy._locals = { ...(localCopy.locals || {}) };
localCopy._data = { ...localCopy.data };
// Evaluate parents
if (modelData.parent) {
localCopy._parent = this.evaluateModel(modelData.parent);
// Overwrite / combine parent locals with current locals
localCopy._locals = {
...(localCopy._parent._locals || {}),
...localCopy._locals,
};
// Overwrite / combine parent data with current data
localCopy._data = {
...(localCopy._parent._data || {}),
...localCopy._data,
};
// Inherit texture parameters
localCopy.texture = {
...(localCopy._parent?.texture || {}),
...(localCopy.texture || {}),
};
}
// Overwrite / merge locals
if (useLocals) {
localCopy._locals = {
...localCopy._locals,
...useLocals,
};
}
// Evaluate locals with globally combined parameters
this.evaluateLocals(localCopy);
// Recursively substitute all locals within the data.
// Also, fill all slots.
localCopy._dataEvaluated = this.recursiveSubstitute(
localCopy._data,
localCopy._localsEvaluated,
);
return localCopy;
}
/**
* Evaluate a MSON expression (arithmetic)
* @param expression Mson Expression definition
* @param variables Scope variables
*/
protected evaluateExpression(
expression: MsonExpression,
variables: MsonVariables = {},
) {
if (!this.isExpression(expression)) {
throw new Error('Invalid expression');
}
let [src, oper, trg] = expression;
if (this.isExpression(src)) {
// TODO: resolve as unknown, probably with a tuple of some kind
src = this.evaluateExpression(
src as unknown as MsonExpression,
variables,
);
}
if (this.isExpression(trg)) {
// TODO: resolve as unknown, probably with a tuple of some kind
trg = this.evaluateExpression(
trg as unknown as MsonExpression,
variables,
);
}
src = this.substitute(src, variables);
trg = this.substitute(trg, variables);
// Assertions
if (isNaN(src)) {
throw new Error(
`Expression evaluation [${expression}] failed: ${src} is NaN`,
);
}
if (isNaN(trg)) {
throw new Error(
`Expression evaluation [${expression}] failed: ${trg} is NaN`,
);
}
if (!['+', '-', '*', '/', '%', '^'].includes(oper)) {
throw new Error(
`Expression evaluation [${expression}] failed: ${oper} is not supported`,
);
}
// Operators
switch (oper) {
case '+':
return src + trg;
case '-':
return src - trg;
case '*':
return src * trg;
case '/':
return src / trg;
case '%':
return src % trg;
case '^':
return Math.pow(src, trg);
}
}
/**
* Determine if input is an expression or not.
* @param input Questionable input
* @returns Is expression or not
*/
protected isExpression(input: MsonVariable | MsonExpression) {
return (
Array.isArray(input) &&
input.length === 3 &&
typeof input[1] === 'string' &&
!input[1].startsWith('#')
);
}
/**
* Substitute input with variables or return input.
* @param input Input to substitute
* @param variables Pool of variables
* @returns Substituted value
*/
protected substitute(input: MsonVariable, variables: MsonVariables = {}) {
if (typeof input !== 'string') return input;
const key = input.replace('#', '');
return variables[key] ?? input;
}
/**
* Substitute ALL variables recursively.
* Also fills slots. (TODO: separate)
* @param input Model data, a component or component metadata
* @param variables Variables to use
* @returns Substituted object
*/
protected recursiveSubstitute(
input: MsonData | MsonComponent,
variables: MsonVariables = {},
) {
const keyList = Object.keys(input);
// Handle slots, include the child model with current locals already applied.
const asSlot = input as MsonSlot;
if (keyList.includes('data') && typeof asSlot.data === 'string') {
const evaluateChild = this.evaluateModel(
asSlot.data as string,
asSlot.locals
? this.evaluateLocalsObject(asSlot.locals, variables)
: variables,
);
return {
type: 'mson:slot',
texture: evaluateChild.texture,
implementation: asSlot.implementation,
data: evaluateChild._dataEvaluated,
};
}
return keyList.reduce<any>((item, key) => {
const value = item[key];
// Do not substitute locals or generated values here.
if (key === 'locals' || key.startsWith('_')) return item;
if (isObject(value)) {
item[key] = this.recursiveSubstitute(value, variables);
}
if (typeof value === 'string' && value.startsWith('#')) {
item[key] = this.substitute(value, variables);
}
if (this.isExpression(value)) {
item[key] = this.evaluateExpression(value, variables);
}
if (Array.isArray(value)) {
item[key] = value.map((entry) =>
isObject(entry)
? this.recursiveSubstitute(entry, variables)
: this.substitute(entry, variables),
);
}
return item;
}, input);
}
/**
* Evaluate locals on model
* @param localCopy Local evaluated model
*/
protected evaluateLocals(localCopy: MsonEvaluatedModel) {
if (!localCopy._locals) {
if (localCopy._parent?._localsEvaluated) {
// Copy parent's globals
localCopy._localsEvaluated = deepClone(
localCopy._parent._localsEvaluated,
);
}
// Nothing more to evaluate
return;
}
localCopy._localsEvaluated = this.evaluateLocalsObject(
localCopy._locals,
localCopy._parent?._localsEvaluated,
);
}
/**
* Substitute locals' variables and evaluate expressions.
* @param locals Locals to evaluate
* @param parentEvals Parents evaluated locals, falls back to these.
* @returns Locals with variables substituted.
*/
protected evaluateLocalsObject(
locals: MsonLocals,
parentEvals: MsonEvaluatedLocals = {},
) {
return Object.keys(locals).reduce<MsonEvaluatedLocals>(
(pool, local) => {
const currentValue = locals?.[local];
if (currentValue === undefined) return pool;
// Number type just pass on
if (typeof currentValue === 'number') {
pool[local] = currentValue;
}
// Substitute strings, those are variables
if (typeof currentValue === 'string') {
pool[local] = this.substitute(currentValue, pool);
}
// Evaluate expressions
if (this.isExpression(currentValue)) {
pool[local] = this.evaluateExpression(
currentValue as MsonExpression,
pool,
);
}
return pool;
},
{ ...parentEvals },
);
}
}