/*
* This file is part of TREB.
*
* TREB is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with TREB. If not, see .
*
* Copyright 2022-2026 trebco, llc.
* info@treb.app
*
*/
import type { FunctionLibrary } from './function-library';
import type { Cell, ICellAddress,
UnionValue, CellValue,
ArrayUnion,
NumberUnion,
UndefinedUnion,
ComplexUnion,
DimensionedQuantityUnion,
IArea,
FunctionUnion} from 'treb-base-types';
import { ValueType, GetValueType, Area } from 'treb-base-types';
import type { Parser, ExpressionUnit, UnitBinary, UnitIdentifier,
UnitGroup, UnitUnary, UnitAddress, UnitRange, UnitCall, UnitDimensionedQuantity, UnitStructuredReference,
UnitImplicitCall,
UnitArray} from 'treb-parser';
import type { DataModel, MacroFunction, Sheet } from 'treb-data-model';
import { NameError, ReferenceError, ExpressionError, UnknownError, SpillError, ValueError, ArgumentError } from './function-error';
import * as Primitives from './primitives';
import type { ContextResult } from './descriptors';
//////////
/**
* dynamically adding a user data field to the expression so we can
* cache a function call, avoiding type switching and function lookups.
*
* we use the generic type so we can cover the composite type as well
* before specifying
*/
type AttachCachedFunction = (T & { fn: (arg0: T) => UnionValue });
/**
* expression unit with cached function
*/
type ExpressionWithCachedFunction = T extends { type: T['type'] } ? AttachCachedFunction : never;
/**
* @internal
*/
export type ExtendedExpressionUnit = ExpressionWithCachedFunction;
// FIXME: move
// FIXME: this is sloppy
export const UnionIsExpressionUnit = (test: UnionValue /*UnionOrArray*/): test is { type: ValueType.object, value: ExpressionUnit } => {
return !Array.isArray(test)
&& test.type === ValueType.object
&& (!!(test.value as ExpressionUnit).type);
};
// FIXME: move
// FIXME: this is sloppy
export const UnionIsMetadata = (test: UnionValue /*UnionOrArray*/): test is { type: ValueType.object, value: ReferenceMetadata } => {
return test.type === ValueType.object && test.key === 'metadata';
};
// FIXME: move
export interface ReferenceMetadata {
type: 'metadata';
// what's the context in which I was using the unit address (parse expression?)
address: UnitAddress; // ICellAddress;
value: CellValue;
format?: string;
}
export type BindingFrame = Record; // FIXME (type?)
export type PositionalFrame = ExpressionUnit[];
export interface CalculationContext {
address: ICellAddress;
area?: IArea;
volatile: boolean;
/** new for lambdas */
bindings: BindingFrame[];
}
export class ExpressionCalculator {
public context: CalculationContext = {
address: { row: -1, column: -1 },
volatile: false,
bindings: [],
};
// --- public API -----------------------------------------------------------
constructor(
protected readonly data_model: DataModel,
protected readonly library: FunctionLibrary,
protected readonly parser: Parser) {}
/**
* there's a case where we are calling this from within a function
* (which is weird, but hey) and to do that we need to preserve flags.
*/
public Calculate(expr: ExpressionUnit, addr: ICellAddress, area?: IArea, preserve_flags = false): {
value: UnionValue /*UnionOrArray*/, volatile: boolean }{
if (!preserve_flags) {
this.context.address = addr;
this.context.volatile = false;
this.context.area = area;
}
return {
value: this.CalculateExpression(expr as ExtendedExpressionUnit),
volatile: this.context.volatile,
};
}
// --- /public API ----------------------------------------------------------
/**
* resolve value from cell. returns a function bound to specific cell.
*/
protected CellFunction2(expr: UnitAddress): () => UnionValue {
if (!expr.sheet_id) {
if (expr.sheet) {
expr.sheet_id = this.data_model.sheets.ID(expr.sheet) || 0;
}
else {
return () => ReferenceError();
}
}
const cells = this.data_model.sheets.Find(expr.sheet_id)?.cells;
if (!cells) {
console.warn('missing cells reference @ ' + expr.sheet_id);
return () => ReferenceError();
}
// reference
const cell = cells.GetCell(expr);
// this is not an error, just a reference to an empty cell
// FIXME: should this be 0? probably
if (!cell) {
return () => {
return { type: ValueType.number, value: 0 };
};
}
if (expr.spill && cell.spill && cell.spill.start.row === expr.row && cell.spill.start.column === expr.column) {
return () => {
return cell.spill ? cells.GetRange4(cell.spill.start, cell.spill.end, true) || ReferenceError() : SpillError();
}
}
// close
return () => cell.GetValue4();
}
/**
* returns range as union type. returns a single value for a single cell,
* or a 2d array (never a 1d array)
*/
protected CellFunction4(start: ICellAddress, end: ICellAddress): UnionValue /*UnionOrArray*/ {
if (!start.sheet_id) {
return ReferenceError();
// throw new Error('missing sheet id in CellFunction4');
}
const cells = this.data_model.sheets.Find(start.sheet_id)?.cells;
return cells?.GetRange4(start, end, true) || ReferenceError();
}
/** breaking this out to de-dupe */
protected GetMetadata(arg: ExpressionUnit, transform: (cell_data: Cell, address: ICellAddress) => T): UnionValue {
// FIXME: we used to restrict this to non-cell functions, now
// we are using it for the cell function (we used to use address,
// which just returns the label)
let address: ICellAddress|undefined;
let range: {start: ICellAddress; end: ICellAddress} | undefined;
switch (arg.type) {
case 'address':
if (arg.spill) {
let sheet: Sheet|undefined;
if (arg.sheet_id) {
sheet = this.data_model.sheets.Find(arg.sheet_id);
}
if (!sheet) {
console.error('missing sheet [da6]');
return ReferenceError();
}
const cell_data = sheet.CellData(arg);
if (cell_data.spill) {
range = cell_data.spill;
}
}
else {
address = arg;
}
break;
case 'range':
range = arg;
break;
case 'structured-reference':
{
const resolved = this.data_model.ResolveStructuredReference(arg, this.context.address);
if (resolved) {
if (resolved.type === 'address') {
address = resolved;
}
else if (resolved.type === 'range') {
range = resolved;
}
}
}
break;
case 'identifier':
{
const named_range = this.data_model.GetName(arg.name, this.context.address.sheet_id || 0);
if (named_range?.type === 'range') {
if (named_range.area.count === 1) {
address = named_range.area.start; // FIXME: range?
}
else {
range = named_range.area;
}
}
}
break;
case 'call':
// we need a way to cascade the 'metadata' flag down
// through calls so we can use indirect/offset addressing...
// at the same time you don't want to cascade down indefinitely,
// otherwise the function call itself won't work properly...
// [how to resolve?]
{
const result = this.CalculateExpression(arg as ExtendedExpressionUnit, true) as UnionValue /*UnionOrArray*/;
if (UnionIsExpressionUnit(result)) {
if (result.value.type === 'address') {
address = result.value;
}
else if (result.value.type === 'range') {
range = result.value;
}
else {
return result;
}
}
else return result;
}
break;
default:
return this.CalculateExpression(arg as ExtendedExpressionUnit); // as UnionOrArray;
}
if (address) {
// don't we have a map? [...] only for names?
let sheet: Sheet|undefined;
if (address.sheet_id) {
sheet = this.data_model.sheets.Find(address.sheet_id);
}
if (!sheet) {
console.error('missing sheet [ac8]');
return ReferenceError();
}
const cell_data = sheet.CellData(address);
const value = cell_data.calculated_type ? cell_data.calculated : cell_data.value;
const metadata: ReferenceMetadata = {
type: 'metadata',
// metadata is expecting a parse expression instead of an addresss.
// note we're not setting the label properly here, which could be
// an issue? not sure who's calling it in this case
// UPDATE: "Cell" is calling it, so it needs a label
address: {
...address,
position: 0,
id: 0,
type: 'address',
label: new Area(address).spreadsheet_label,
},
value,
format: cell_data.style ? cell_data.style.number_format : undefined,
...transform(cell_data, address),
};
return { type: ValueType.object, value: metadata, key: 'metadata' };
}
else if (range) {
if (range.start.row === Infinity || range.start.column === Infinity) {
return ReferenceError();
}
let sheet: Sheet|undefined;
if (range.start.sheet_id) {
sheet = this.data_model.sheets.Find(range.start.sheet_id);
}
if (!sheet) {
throw new Error('missing sheet [ac9]');
}
const range_result: UnionValue[][] = [];
for (let column = range.start.column; column <= range.end.column; column++) {
const column_result: UnionValue[] = [];
for (let row = range.start.row; row <= range.end.row; row++) {
const cell_data = sheet.CellData({row, column});
address = {...range.start, row, column};
const value = cell_data.calculated_type ? cell_data.calculated : cell_data.value;
const metadata = {
type: 'metadata',
address,
value,
format: cell_data.style ? cell_data.style.number_format : undefined,
...transform(cell_data, address),
};
column_result.push({
type: ValueType.object,
value: metadata,
key: 'metadata',
});
}
range_result.push(column_result);
}
return {type: ValueType.array, value: range_result};
}
return this.CalculateExpression(arg as ExtendedExpressionUnit); /*UnionOrArray*/
}
protected RewriteMacro(
unit: ExpressionUnit,
names: Record,
): ExpressionUnit {
let expr: ExpressionUnit;
switch (unit.type) {
case 'identifier':
expr = names[unit.name.toUpperCase()];
if (expr) {
return JSON.parse(JSON.stringify(expr)) as ExpressionUnit;
}
break;
case 'binary':
unit.left = this.RewriteMacro(unit.left, names);
unit.right = this.RewriteMacro(unit.right, names);
break;
case 'unary':
unit.operand = this.RewriteMacro(unit.operand, names);
break;
case 'group':
unit.elements = unit.elements.map(element => this.RewriteMacro(element, names));
break;
case 'call':
unit.args = unit.args.map(arg => this.RewriteMacro(arg, names));
break;
}
return unit;
}
protected CallMacro(outer: UnitCall, macro: MacroFunction): (expr: UnitCall) => UnionValue /*UnionOrArray*/ {
if (!macro.expression) {
return () => ExpressionError();
}
const text_expr = JSON.stringify(macro.expression);
const names: Record = {};
const upper_case_names = macro.argument_names?.map(name => name.toUpperCase()) || [];
return (expr: UnitCall) => {
const clone = JSON.parse(text_expr);
for (let i = 0; i < upper_case_names.length; i++) {
names[upper_case_names[i]] = expr.args[i] || { type: 'missing', id: 0 };
}
return this.CalculateExpression(this.RewriteMacro(clone, names) as ExtendedExpressionUnit);
}
}
/**
* split out from ImplicitCall so we can reuse
*/
public ImplicitCallTail(result: FunctionUnion, args: ExpressionUnit[]) {
const value = result.value as {
bindings: ExpressionUnit[];
func: ExpressionUnit|undefined;
};
if (!value.func || !value.bindings) {
return ExpressionError();
}
const frame: BindingFrame = {};
for (let i = 0; i < value.bindings.length; i++) {
const name = value.bindings[i];
if (name?.type === 'identifier') {
frame[name.name.toUpperCase()] = args[i] || { type: 'missing' };
}
else {
// should not happen, error
return ExpressionError();
}
}
const munged = JSON.parse(JSON.stringify(value.func));
this.parser.Walk2(munged, (unit: ExpressionUnit) => {
if (unit.type === 'identifier') {
const upper_case = unit.name.toUpperCase();
const binding = frame[upper_case];
if (binding) {
return JSON.parse(JSON.stringify(binding));
}
}
return true;
});
return this.CalculateExpression(munged as ExtendedExpressionUnit);
}
/**
* an FP call will need to set bindings and call an expression,
* possibly multiple times. this is a support function for that.
*/
protected Apply(fn: FunctionUnion, args: UnionValue[]) {
// kind of going backwards here, converting values to expressions...
// the reason is that we rewrite lambdas to support recursion
const mapped: ExpressionUnit[] = args.map(arg => {
switch (arg.type) {
case ValueType.number:
case ValueType.boolean:
case ValueType.string:
return {
type: 'literal', value: arg.value, id: 0, position: 0,
}
case ValueType.error:
return {
type: 'literal', value: '#' + arg.value, id: 0, position: 0,
}
case ValueType.array:
{
const values: UnitArray['values'] = [];
for (let c = 0; c < arg.value.length; c++) {
const col = arg.value[c];
const mapped_col: UnitArray['values'][0] = [];
for (let r = 0; r < col.length; r++ ) {
const val = col[r];
switch (val.type) {
case ValueType.boolean:
case ValueType.number:
case ValueType.string:
mapped_col.push(val.value || undefined);
break;
default:
console.warn('unhandled array value', val);
mapped_col.push(undefined);
}
}
values.push(mapped_col);
}
return {
type: 'array',
values,
id: 0, position: 0,
};
}
case ValueType.undefined:
return {
type: 'missing', id: 0,
};
default:
// this (logging) is a problem in a simulation because
// it can bog down. we probably should only log once.
// or perhaps not at all?
console.warn('unhandled parameter value', arg);
}
return { type: 'missing', id: 0 };
});
return this.ImplicitCallTail(fn, mapped);
}
/**
* this method can take a `call` type if it looked like a call at
* the parsing stage, it's a simple translation between the two
*/
protected ImplicitCall(): (expr: UnitImplicitCall|UnitCall) => UnionValue {
return (expr: UnitImplicitCall|UnitCall) => {
if (expr.type === 'call') {
expr = {
type: 'implicit-call',
args: expr.args,
call: {
type: 'identifier',
name: expr.name,
position: expr.position,
id: 0,
},
position: expr.position,
id: 0,
};
}
const result = this.CalculateExpression(expr.call as ExtendedExpressionUnit);
if (result.type === ValueType.function) {
return this.ImplicitCallTail(result, expr.args)
}
return ExpressionError();
};
}
protected NormalizeBindings(context: BindingFrame) {
const frame: Record = {};
for (const [key, value] of Object.entries(context)) {
frame[key.toUpperCase()] = value;
}
return frame;
}
/**
* excute a function call
*/
protected CallExpression(outer: UnitCall, return_reference = false): (expr: UnitCall) => UnionValue /*UnionOrArray*/ {
// get the function descriptor, which won't change.
// we can bind in closure (also short-circuit check for
// invalid name)
const func = this.library.Get(outer.name);
if (!func) {
const upper_case = outer.name.toUpperCase();
const binding = this.LookupBinding(upper_case);
if (binding) {
return this.ImplicitCall();
}
const named = this.data_model.GetName(upper_case, this.context.address.sheet_id || 0);
if (named) {
return this.ImplicitCall();
}
if (process.env.NODE_ENV !== 'production') {
console.info('(dev) missing function', outer.name);
}
return () => NameError();
}
return (expr: UnitCall) => {
// set context volatile if this function is volatile. it will bubble
// through nested function calls, so the entire cell will be volatile
// if there's a volatile function in there somewhere
this.context.volatile = this.context.volatile || (!!func.volatile);
// we recurse calculation, but in the specific case of IF functions
// we can short-circuit and skip the unused code path. doesn't apply
// anywhere else atm
const if_function = outer.name.toLowerCase() === 'if';
let skip_argument_index = -1;
let argument_error: UnionValue|undefined;
// possibly create a binding frame. if we do that we may need
// to adjust the arguments as well (and the descriptors)
let args = expr.args;
let argument_descriptors = func.arguments || []; // map
let binding: ContextResult|undefined;
if (func.create_binding_context) {
// if this does not return a binding frame, it's an error.
binding = func.create_binding_context.call(0, {
args: expr.args,
descriptors: argument_descriptors,
});
if (binding) {
args = binding.args;
if (binding.argument_descriptors) {
argument_descriptors = binding.argument_descriptors;
}
this.context.bindings.unshift(this.NormalizeBindings(binding.context));
}
else {
argument_error = ArgumentError();
}
}
const mapped_args = args.map((arg, arg_index) => {
// short circuit
if (argument_error) {
return undefined;
}
// get descriptor. if the number of arguments exceeds
// the number of descriptors, recycle the last one
// FIXME: we have a repeat flag, so we should repeat the
// correct argument(s). I guess we could default to this
// behavior.
const descriptor = argument_descriptors[Math.min(arg_index, argument_descriptors.length - 1)] || {};
// new for lambdas
if (descriptor.passthrough) {
return arg;
}
// if function, wrong branch
if (arg_index === skip_argument_index) {
return descriptor.boxed ? { type: ValueType.undefined } : undefined;
}
// note on type here: we're iterating over the arguments
// described by the parse expression, not the values. although
// in this case, wouldn't this be a missing type? (...)
if (typeof arg === 'undefined') {
if (if_function && arg_index === 0) { skip_argument_index = 1; }
return descriptor.boxed ? { type: ValueType.undefined } : undefined;
}
// FIXME (address): what about named ranges (actually those will work),
// constructed references (we don't support them atm)?
// NOTE: named ranges will _not_ work, because the address will be an
// object, not a string. so FIXME.
if (descriptor.address) {
return descriptor.boxed ? {
type: ValueType.string,
value: this.parser.Render(arg).replace(/\$/g, ''),
} : this.parser.Render(arg).replace(/\$/g, '');
}
else if (descriptor.metadata) {
return this.GetMetadata(arg, () => { return {}}); // type is UnionOrArray
}
else {
const result = this.CalculateExpression(arg as ExtendedExpressionUnit);
if (result.type === ValueType.error) { // array check is implicit since array is a type
if (descriptor.allow_error) {
return result; // always boxed
}
argument_error = result;
return undefined; // argument not used, so don't bother boxing
}
// can't shortcut if you have an array (or we need to test all the values)
if (if_function && arg_index === 0 && result.type !== ValueType.array){
let result_truthy = false;
if (result.type === ValueType.string) {
const lowercase = (result.value as string).toLowerCase().trim();
result_truthy = lowercase !== 'false' && lowercase !== 'f';
}
else {
result_truthy = !!result.value;
}
skip_argument_index = result_truthy ? 2 : 1;
}
if (descriptor.boxed) {
return result;
}
if (result.type === ValueType.array) {
return (result as ArrayUnion).value.map(row => row.map(value => value.value));
}
else {
return result.value; // unboxing
}
}
});
if (binding) {
this.context.bindings.shift();
}
if (argument_error) {
return argument_error;
}
// cloning, out of an abundance of caution
// const ctx = JSON.parse(JSON.stringify({ address: this.context.address, area: this.context.area }));
const ctx = {
address: { ...this.context.address },
area: this.context.area ? {
start: { ...this.context.area.start, },
end: { ...this.context.area.end, },
} : undefined,
apply: func.fp ? this.Apply.bind(this) : undefined,
};
const result = func.fn.apply(ctx, mapped_args);
if (func.return_type === 'reference') {
if (return_reference) {
return result;
}
if (UnionIsExpressionUnit(result)) {
if (result.value.type === 'address') {
return this.CellFunction2(result.value)();
}
else if (result.value.type === 'range') {
return this.CellFunction4(result.value.start, result.value.end)
}
}
return result; // error?
}
return result; // func.fn.apply(ctx, mapped_args);
};
}
protected ResolveStructuredReference(expr: UnitStructuredReference): () => UnionValue {
// basically our approach here is to resolve the structured reference
// to a concrete reference.
//
// if the structured reference changes, then it will get recalculated
// (and hence rebuilt). if the table name or a referenced column name
// changes, the cell will get rewritten so again, it will get recalculated.
//
// the case we have to worry about is if the table layout changes: if a
// column is added or removed. because in that case, our reference will
// be out of date but we won't be notified about it.
//
// so we will have to make sure that if a table layout changes, columns
// or rows added or deleted, then we invalidate the entire table. if we
// do that this should all work out.
const resolved = this.data_model.ResolveStructuredReference(expr, this.context.address);
if (resolved) {
if (resolved.type === 'address') {
return this.CellFunction2(resolved);
}
else if(resolved.type === 'range') {
return () => this.CellFunction4(resolved.start, resolved.end);
}
}
return () => ReferenceError();
}
protected ResolveDimensionedQuantity(): (exp: UnitDimensionedQuantity) => UnionValue {
return (expr: UnitDimensionedQuantity): UnionValue => {
const expression = this.CalculateExpression(expr.expression as ExtendedExpressionUnit);
return {
type: ValueType.dimensioned_quantity,
value: {
value: expression.value,
unit: expr.unit.name,
},
} as DimensionedQuantityUnion;
};
}
protected UnaryExpression(x: UnitUnary, return_reference = false): (expr: UnitUnary) => UnionValue /*UnionOrArray*/ { // operator: string, operand: any){
// there are basically three code paths here: negate, identity, and error.
// they have very different semantics so we're going to do them completely
// separately.
switch (x.operator) {
case '+':
return (expr: UnitUnary) => {
return this.CalculateExpression(expr.operand as ExtendedExpressionUnit);
};
case '-':
{
const func = Primitives.Subtract;
const zero = { type: ValueType.number, value: 0 } as NumberUnion;
return (expr: UnitUnary) => {
const operand = this.CalculateExpression(expr.operand as ExtendedExpressionUnit);
if (operand.type === ValueType.array) {
return {
type: ValueType.array,
value: (operand as ArrayUnion).value.map(column => column.map(value => func(zero, value))),
};
}
return func(zero, operand);
};
}
case '@':
return (expr: UnitUnary) => {
// if the operand is a range, then we need to do implicit intersection.
// otherwise, calculate the expression and return the first value
// if it's an array.
let address: UnitAddress|undefined;
switch (expr.operand.type) {
case 'address':
if (expr.operand.spill) {
// we need to calculate the result so we know how large the
// range is... perhaps there's a way to look this up without
// calculating? (TODO/FIXME)
const calculated = this.CellFunction2(expr.operand)();
if (calculated.type === ValueType.array) {
const row = this.context.address.row ?? -1;
const column = this.context.address.column ?? -1;
// for this verison we already have the result, so unless
// we're trying to preserve the address, we could just
// return it
if (row >= expr.operand.row && row < expr.operand.row + calculated.value[0]?.length && calculated.value.length === 1) {
if (!return_reference) {
return calculated.value[0][row - expr.operand.row];
}
address = {
...expr.operand,
row,
spill: false,
};
}
else if (column >= expr.operand.column && column < expr.operand.column + calculated.value.length && calculated.value[0]?.length === 1) {
if (!return_reference) {
return calculated.value[column - expr.operand.column][0];
}
address = {
...expr.operand,
column,
spill: false,
};
}
else {
return ValueError(); // out of range
}
}
// return { type: ValueType.string, value: 'implicit (spill)' };
}
break;
case 'range':
{
// how do we intersect, if at all?
const row = this.context.address.row ?? -1;
const column = this.context.address.column ?? -1;
if (row >= expr.operand.start.row && row <= expr.operand.end.row && expr.operand.start.column === expr.operand.end.column) {
address = {
...expr.operand.start,
row,
spill: false,
};
}
else if (column >= expr.operand.start.column && column <= expr.operand.end.column && expr.operand.start.row === expr.operand.end.row) {
address = {
...expr.operand.start,
column,
spill: false,
};
}
else {
return ValueError(); // out of range
}
}
}
if (address) {
if (return_reference) {
return { type: ValueType.object, value: address,
}
}
return this.CellFunction2(address)();
}
const operand = this.CalculateExpression(expr.operand as ExtendedExpressionUnit);
if (operand.type === ValueType.array) {
return operand.value[0][0];
}
return operand;
};
default:
return () => {
console.warn('unexpected unary operator:', x.operator);
return ExpressionError();
};
}
}
/**
* expands the size of an array by recycling values in columns and rows
*
* FIXME: seems like this is more a generic thing, -> utils lib
*
* @param arr 2d array
* @param columns target columns
* @param rows target rows
*/
protected RecycleArray(arr: T[][], columns: number, rows: number): T[][] {
// NOTE: recycle rows first, more efficient. do it in place?
if (arr[0].length < rows) {
const len = arr[0].length;
for (const column of arr) {
for (let r = len; r < rows; r++ ) {
column[r] = column[r % len];
}
}
}
if (arr.length < columns) {
const len = arr.length;
for (let c = len; c < columns; c++) arr[c] = arr[c % len].slice(0);
}
return arr;
}
protected ElementwiseBinaryExpression(fn: Primitives.PrimitiveBinaryExpression, left: ArrayUnion, right: ArrayUnion): ArrayUnion {
const columns = Math.max(left.value.length, right.value.length);
const rows = Math.max(left.value[0].length, right.value[0].length);
const left_values = this.RecycleArray(left.value, columns, rows);
const right_values = this.RecycleArray(right.value, columns, rows);
const value: UnionValue[][] = [];
for (let c = 0; c < columns; c++) {
const col: UnionValue[] = [];
for (let r = 0; r < rows; r++ ) {
// handle undefineds. this is unfortunate. shouldn't the recycle
// function do that? ...CHECK/TODO/FIXME
col[r] = fn(
left_values[c][r] || { type: ValueType.undefined },
right_values[c][r] || { type: ValueType.undefined });
}
value.push(col);
}
return { type: ValueType.array, value };
}
/**
* convert expr to a cell address, possibly calculating the contents.
* returns single address only (ranges with len > 1 will fail)
*/
protected AsReference(expr: ExpressionUnit): ICellAddress|undefined {
switch (expr.type) {
case 'address':
return expr;
case 'range':
if (expr.start.row === expr.end.row && expr.start.column === expr.end.column) {
return expr.start;
}
break;
default:
{
const union = this.CalculateExpression(expr as ExtendedExpressionUnit, true);
if (UnionIsExpressionUnit(union)) {
if (union.value.type === 'address') {
return union.value;
}
if (union.value.type === 'range' && union.value.start.row === union.value.end.row && union.value.start.column === union.value.end.column) {
return union.value.start;
}
}
}
break;
}
return undefined;
}
protected BinaryExpression(x: UnitBinary): (expr: UnitBinary) => UnionValue /*UnionOrArray*/ {
// we are constructing and caching functions for binary expressions.
// this should simplify calls when parameters change. eventually I'd
// like to do this for other dynamic calls as well...
// the idea is that we can start composing compound expressions. still
// not sure if that will work (or if it's a good idea).
// NOTE (for the future?) if one or both of the operands is a literal,
// we can bind that directly. literals in the expression won't change
// unless the expression changes, which will discard the generated
// function (along with the expression itself).
// pulling out concat so we can bind the model for language values
const fn = x.operator === '&' ? (a: UnionValue, b: UnionValue): UnionValue => {
// this works, but it's not volatile so it doesn't update on
// a language change; maybe language change should force a recalc? (...)
if (a.type === ValueType.error) { return a; }
if (b.type === ValueType.error) { return b; }
const strings = [a, b].map(x => {
if (x.type === ValueType.undefined) { return ''; }
if (x.type === ValueType.boolean) {
if (x.value) {
return this.data_model.language_model?.boolean_true || 'TRUE';
}
else {
return this.data_model.language_model?.boolean_false || 'FALSE';
}
}
return x.value;
});
return {
type: ValueType.string,
value: `${strings[0]}${strings[1]}`,
};
} : Primitives.MapOperator(x.operator);
if (!fn) {
// support dynamically-constructed ranges, as long as the
// arguments are both addresses (we might see ranges, but
// if they are 1x1 then we can accept them)
if (x.operator === ':') {
return (expr: UnitBinary) => {
const start = this.AsReference(expr.left);
const end = this.AsReference(expr.right);
if (start && end) {
return this.CellFunction4(start, end);
}
return ExpressionError();
};
}
return () => { // expr: UnitBinary) => {
console.info(`(unexpected binary operator: ${x.operator})`);
return ExpressionError();
};
}
else {
return (expr: UnitBinary) => {
// sloppy typing, to support operators? (...)
const left = this.CalculateExpression(expr.left as ExtendedExpressionUnit);
const right = this.CalculateExpression(expr.right as ExtendedExpressionUnit);
// check for arrays. do elementwise operations.
if (left.type === ValueType.array) {
if (right.type === ValueType.array) {
return this.ElementwiseBinaryExpression(fn, left as ArrayUnion, right as ArrayUnion);
}
return this.ElementwiseBinaryExpression(fn, left as ArrayUnion, {type: ValueType.array, value: [[right]]});
}
else if (right.type === ValueType.array) {
return this.ElementwiseBinaryExpression(fn, {type: ValueType.array, value: [[left]]}, right as ArrayUnion);
}
return fn(left, right);
};
}
}
protected Identifier(expr: UnitIdentifier): () => UnionValue /*UnionOrArray*/ {
// NOTE: TRUE and FALSE don't get here -- they are converted
// to literals by the parser? (...)
// the function we create here binds the name because
// this is a literal identifier. if the value were to change,
// the expression would be discarded.
// however we have to do the lookup dynamically because the
// underlying reference (in the named range map) might change.
// although it's worth noting that, atm at least, that wouldn't
// trigger an update because it's not considered a value change.
// you'd have to recalc, which would rebuild the expression anyway.
// call that a FIXME? (...)
const identifier = expr.name;
// anything starting with # is an error. the only thing we should
// have is #REF, but maybe that will change in the future.
if (identifier[0] === '#') {
return () => ReferenceError();
}
const upper_case = identifier.toUpperCase();
switch (upper_case){
case 'FALSE':
// case 'F':
return () => {return {value: false, type: ValueType.boolean}};
case 'TRUE':
// case 'T':
return () => {return {value: true, type: ValueType.boolean}};
case 'UNDEFINED':
return () => {return {value: undefined, type: ValueType.undefined}}; // why do we support this?
}
return () => {
const binding = this.LookupBinding(upper_case);
if (binding) {
return this.CalculateExpression(binding as ExtendedExpressionUnit);
}
const named = this.data_model.GetName(upper_case, this.context.address.sheet_id || 0);
switch (named?.type) {
case 'range':
if (named.area.count === 1) {
return this.CellFunction4(named.area.start, named.area.start);
}
return this.CellFunction4(named.area.start, named.area.end);
case 'expression':
return this.CalculateExpression(named.expression as ExtendedExpressionUnit);
}
// console.info( '** identifier', {identifier, expr, context: this.context});
return NameError();
};
}
/**
* look up an identifier in any binding frames. we do LIFO binding.
*
* @param name
* @returns
*/
protected LookupBinding(name: string) {
name = name.toUpperCase();
for (const frame of this.context.bindings) {
const value = frame[name];
if (value) {
return value;
}
}
return undefined;
}
protected GroupExpression(x: UnitGroup): (expr: UnitGroup) => UnionValue /*UnionOrArray*/ {
// a group is an expression in parentheses, either explicit
// (from the user) or implicit (created to manage operation
// priority, order of operations, or similar).
// expressions nest, so there's no case where a group should
// have length !== 1 -- consider that an error.
if (!x.elements || x.elements.length !== 1){
console.warn( `Can't handle group !== 1` );
return () => ExpressionError();
}
return (expr: UnitGroup) => this.CalculateExpression(expr.elements[0] as ExtendedExpressionUnit);
}
protected CalculateExpression(expr: ExtendedExpressionUnit, return_reference = false): UnionValue {
// user data is a generated function for the expression, at least
// for the simple ones (atm). see BinaryExpression for more. the
// aim is to remove as many tests and lookups as possible.
// may be over-optimizing here.
if ((expr as AttachCachedFunction).fn) {
return (expr as AttachCachedFunction).fn(expr);
}
switch (expr.type){
case 'implicit-call':
return (expr.fn = this.ImplicitCall())(expr);
case 'call':
{
const macro = this.data_model.macro_functions.get(expr.name.toUpperCase());
if (macro) {
return (expr.fn = this.CallMacro(expr, macro))(expr);
}
return (expr.fn = this.CallExpression(expr, return_reference))(expr);
}
case 'address':
return (expr.fn = this.CellFunction2(expr))(); // check
case 'range':
return (expr.fn = (x: UnitRange) => this.CellFunction4(x.start, x.end))(expr); // check
case 'binary':
return (expr.fn = this.BinaryExpression(expr))(expr); // check
case 'unary':
return (expr.fn = this.UnaryExpression(expr, return_reference))(expr); // check
case 'identifier':
return (expr.fn = this.Identifier(expr))(); // check
case 'missing':
return (expr.fn = () => { return { value: undefined, type: ValueType.undefined } as UndefinedUnion })(); // check
case 'dimensioned':
return (expr.fn = this.ResolveDimensionedQuantity())(expr);
case 'literal':
{
const literal = { value: expr.value, type: GetValueType(expr.value) } as UnionValue;
return (expr.fn = () => literal)(); // check
}
case 'group':
return (expr.fn = this.GroupExpression(expr))(expr); // check
case 'complex':
{
const literal = {value: {real: expr.real, imaginary: expr.imaginary}, type: ValueType.complex } as ComplexUnion;
return (expr.fn = () => literal)(); // check
}
case 'structured-reference':
return (expr.fn = this.ResolveStructuredReference(expr))();
case 'array':
{
return (expr.fn = () => {
return {
type: ValueType.array,
value: expr.values.map((row) => (Array.isArray(row) ? row : [row]).map((value) => {
return { type: GetValueType(value), value } as UnionValue;
})),
} as ArrayUnion;
})();
}
default:
console.warn( 'Unhandled parse expr:', expr);
return UnknownError();
}
}
}