Skip to content

Excel under the hood

The following has been learned by reverse engineering Office 365 Excel Online.

All Excel functions have a signature of the form

FUNCTION(head_1, ..., head_a, [rest_1_1, ..., rest_1_b, ..., rest_n_1, ..., rest_n_b], tail_1, ... tail_c)

where a >= 0 is the number of head types, b >= 0 is the number of rest types, and c >= 0 is the number of tail types.

If the number of rest types b is above zero, the function is variadic; it can take any number of arguments, as long as the number is a plus a multiple of b. The head types describe any arguments which come before the variadic part of the argument list. Likewise, the tail types describe any arguments which come after.

The following invariants apply to the function signature:

  • minArgs.length >= 0
  • If the number of rest types is zero, then the number of tail types must also be zero: restTypes.length !== 0 || tailTypes.length === 0
  • The number of tail types must be strictly less than the number of rest types: tailTypes.length < restTypes.length
  • If the number of rest types is zero, then the number of head types and tail types must not be less than the number of minimum argmuents: 0 !== restTypes.length || headTypes.length + tailTypes.length >= minArgs.

Validating the number of arguments is as follows (same algorithm, but not directly copied from Excel):

function validateNumArgs(signature, givenArgs) {
const restTypes = signature.restTypes;
const headTypes = signature.headTypes;
const tailTypes = signature.tailTypes;
if (givenArgs.length < signature.minArgs) {
// Insufficient number of arguments
return false;
}
if (restTypes.length === 0 && givenArgs.length > headTypes.length) {
// Excess number of arguments
return false;
}
if (
restTypes.length > 1 &&
givenArgs.length > headTypes.length &&
(givenArgs.length - headTypes.length) % restTypes.length > tailTypes.length
) {
// Insufficient number of arguments
return false;
}
return true;
}

Routine: Get the type of the n-th argument from signature

Section titled “Routine: Get the type of the n-th argument from signature”
function getArgType(funcType, funcArgIdx, funcArgsLength) {
var headTypes = funcType.headTypes;
var restTypes = funcType.restTypes;
var tailTypes = funcType.tailTypes;
msAssertPositiveInteger("calc.runtime.getArgType: n", funcArgsLength);
msAssertNonnegativeIntegerLessThan(
"calc.runtime.getArgType: i",
funcArgIdx,
funcArgsLength
);
if (funcArgIdx < headTypes.length) {
return headTypes[funcArgIdx];
}
if (0 === restTypes.length) {
// If the number of rest types is zero, we know that the number of tail
// types must also be zero. Thus it's safe to return here.
return SignatureType.None;
}
var numTailTypes = (funcArgsLength - headTypes.length) % restTypes.length;
if (numTailTypes > tailTypes.length) {
return SignatureType.None;
}
if (funcArgIdx >= funcArgsLength - numTailTypes) {
return tailTypes[funcArgIdx - (funcArgsLength - numTailTypes)];
} else {
return restTypes[(funcArgIdx - headTypes.length) % restTypes.length];
}
}

Let’s take the SWITCH function as an example, which has one head type, two rest types, and one tail type. From the Microsoft documentation:

SWITCH(expression, value1, result1, [value2, result2] ..., [default])
ArgumentDescription
expression (required)Expression is the value (such as a number, date or some text) that will be compared against value1…value126.
value1…value126ValueN is a value that will be compared against expression.
result1…result126ResultN is the value to be returned when the corresponding valueN argument matches expression. ResultN and must be supplied for each corresponding valueN argument.
default (optional)Default is the value to return in case no matches are found in the valueN expressions. The Default argument is identified by having no corresponding resultN expression (see examples). Default must be the final argument in the function.

The single head type corresponds to expression. Thereafter come an indefinite number of variadic arguments. The arguments at indeces 1, 3, 5, … are values, which have the first rest type. The arguments at indeces 2, 4, 6, … are results, and have the second rest type. Finally, the single tail type corresponds to default, which in this case is optional.

The JavaScript object which describes a function signature is as follows:

function functionType(
minArgs,
headTypes,
restTypes,
tailTypes,
returnType,
staticPrecs,
flags
) {
(msAssertNonnegativeInteger("calc.lang.functionType", minArgs),
0 === restTypes.length
? 0 !== tailTypes.length
: tailTypes.length >= restTypes.length) &&
msAssertFailure595980873(
"calc.lang.functionType: expected tailTypes.length < restTypes.length; given " +
JSON.stringify(tailTypes)
);
0 === restTypes.length &&
headTypes.length + tailTypes.length < minArgs &&
msAssertFailure595980873(
"calc.lang.functionType: expected headTypes.length + tailTypes.length >= " +
minArgs +
"; given " +
JSON.stringify(headTypes)
);
return {
minArgs: minArgs,
headTypes: headTypes,
restTypes: restTypes,
tailTypes: tailTypes,
returnType: returnType,
staticPrecs: staticPrecs,
flags: flags,
};
}

Each attribute of the signature is either a bitfield, or a list of bitfields, except for minargs.

  • minargs: Integer >= 0, the minimum number of arguments.
  • headTypes: A list of OperKind bitfields, one for each head type.
  • restTypes: A list of OperKind bitfields, one for each rest type.
  • tailTypes: A list of OperKind bitfields, one for each tail type.
  • returnType: A single OperKind bitfield describing the return type of the function.
  • staticPrecs: “Special precedents” which are set statically rather than dynamically. A single SpecialPrecs bitfield. This is perhaps the most mysterious part of the signature. Among other things, it marks which functions need the current time, which functions need random data, etc.
  • flags: A single FunctionFlags bitfield.

The OperKind bitfield is as follows:

BitNameComment
1Number
2String
4Boolean
8Error
16Range
32Array
64MissingThe value of an argument which omitted (e.g. SUM(1, , 3, , 5))… maybe?
128BlankThe value of a cell which is blank… probably
256RichRich data types are new in Excel 365. See https://theexcelclub.com/rich-data-types-in-excel/

The SignatureType bitfield suspiciously describes four bit patterns which augmentOperKind:

Bit(s)NameComment
0NoneNo bits are set
512GotoI don’t know what this is
1024Ref3D3D reference
2047AllAll bits are set

The SpecialPrecs bitfields is as follows:

Bit(s)NameComment
0NoneNo bits are set
1OwnDocDoesn’t seem to be used, AFAICT
2OwnSheetDoesn’t seem to be used, AFAICT
4OwnRowSet for ROW function
8OwnColSet for COL function
16OwnRowCopyDoesn’t seem to be used, AFAICT
32OwnColCopyDoesn’t seem to be used, AFAICT
64OwnDocSheetsSet for SHEET, SHEETS functions
128CellProps
256AllCellsSet for OFFSET, INDIRECT, SUMIF, AVERAGEIF, LOOKUP, CELL
512AllNamesDoesn’t seem to be used, AFAICT
1024TimeEnabled if the function requests the current time
2048CalcEnabled if the function requests randomness
4096InFilterModeOnly set dynamically (not statically as a part of function signatures it seems)
8192RefStyle
16383AllAll bits are set

The FunctionFlags bitfield is as follows:

Bit(s)NameComment
0None
1DarkSet for a lot of functions. I have no idea what it means and it doesn’t seem to be used.
2Macro
4OnlyParsedDuringLoad
8Internal
16Allow3dRefAllows 3d references

Calls to functions and operators are all routed through the same infrastructure. For example, the signature types are the same.

To call a function you need three things:

  1. The function ID, an integer in the range [0, 681].
  2. The function’s signature.
  3. The arguments with which you want to evaluate the function.

The procedure is as follows:

  1. You call dereferenceOpers to dereference any function arguments which have the type OperKind.Range but whose corresponding signature type doesn’t have the OperKind.Range bit set.
  2. You create an instance of the FunctionDispatcher class, and call its dispatchFunction method.
  3. You may need to call FunctionDispatcher.handleLambda on the result of dispatchFunction, to evaluate any lambda function calls. Sometimes you know this is not neccessary, if e.g. you’re evaluating a binary operator.

A convenience function exists in the form of FunctionDispatcher.applyFunction, which performs all steps as follows:

FunctionDispatcher.prototype.applyFunction = function (funcId, funcArgValues) {
var signature = getWorksheetFuncType(funcId);
// The call to dereferenceOpers will mutate the funcArgValues array
// if there is anything to dereference. In that case we make a copy.
var copiedFuncArgValues = funcArgValues.some(isRangeOperKind)
? funcArgValues.slice(0)
: funcArgValues;
dereferenceOpers(this, copiedFuncArgValues, signature);
return this.handleLambda(
this.dispatchFunction(my(funcId), signature, false, copiedFuncArgValues)
);
};

Dereferencing happens by calling either dereferenceOper or dereferenceOpers:

function dereferenceOper(funcDispatcher, oper, targetType) {
var anchors = [];
var dereferenced = dereference(funcDispatcher, oper, targetType, anchors);
if (null == dereferenced) {
if (anchors.length > 0) return msThrowEvaluationError(unevaluated(anchors));
var i =
"calc.runtime.dereferenceOper: internal error: expected nonempty anchors; oper = " +
l(oper);
return ulsLogger.ULS.assertTag(
595980819,
u.LogCategory.msoulscat_ES_EWAJS,
!1,
i
);
}
return dereferenced;
}
// Dereference a list of function arguments inline given a signature.
function dereferenceOpers(funcDispatcher, funcArgs, signature) {
var anchors = [];
for (var funcArgIdx = 0; funcArgIdx < funcArgs.length; funcArgIdx += 1) {
var dereferenced = dereference(
funcDispatcher,
funcArgs[funcArgIdx],
getArgType(signature, funcArgIdx, funcArgs.length),
anchors
);
if (dereferenced != null) {
funcArgs[funcArgIdx] = dereferenced;
}
}
if (anchors.length > 0) return msThrowEvaluationError(unevaluated(anchors));
}

these internally call dereference, which contains the core logic. The most important takeaways are the following:

  • dereference takes the target type into account. If the target type contains OperKind.Range as a subtype, no dereferencing takes place.
  • If the value argument is not a range/reference, the value is simply returned.
  • If the target type contains any subtype which is not OperKind.Missing, OperKind.Range, OperKind.Array, and the range is one-by-one (a single cell), we return that single cell.
  • Otherwise, if the target type contains OperKind.Array as a subtype, we return the array which the range refers to.
  • Otherwise, if the above two cases don’t apply, #VALUE! error is returned.
var OperKind_NotRangeArrayOrMissing =
OperKind.Number |
OperKind.String |
OperKind.Boolean |
OperKind.Error |
OperKind.Blank |
OperKind.Rich;
function dereference(funcDispatcher, oper, targetType, anchors) {
// Ranges are allowed, so there's no need to dereference
if (0 != (targetType & OperKind.Range)) {
return oper;
}
// We don't have a range in our hands; nothing to dereference
if (!isRangeOperKind(oper)) {
return oper;
}
var sheets = oper.sheets;
if (!isSheetIndex(sheets)) {
// We have a 3D reference in our hands. Those cannot be dereferenced.
Bm(
"calc.runtime.dereference",
ErrorType.REF,
"3D range, oper = " + l(oper)
);
return refErrorOper;
}
// If the range oper contains multiple items in the `ranges` list, it
// is a union reference. Those cannot be dereferenced.
if (oper.ranges.length !== 1) {
Bm(
"calc.runtime.dereference",
ErrorType.VALUE,
"union range, oper = " + l(oper)
);
return valueErrorOper;
}
var range = oper.ranges[0];
if (0 != (targetType & OperKind_NotRangeArrayOrMissing)) {
if (isGridCell(range)) {
// The range is 1x1, so it's just a single value
return msDereferenceIntoCellValue(
funcDispatcher,
sheetGridCell(sheets, range),
anchors
);
}
// Dynamic arrays are not officially released in Excel Online,
// but they seem to be implemented behind a feature flag :þ
if (
funcDispatcher.locals.implicitIntersect &&
!msIsFeatureEnabled(FeatureName.CalcSupportDynamicArrays) &&
!msIsFeatureEnabled(FeatureName.CalcPreventSpillingHack)
) {
var originGridCell = funcDispatcher.locals.originGridCell;
if (!isSuccess(originGridCell))
return msThrowEvaluationError(originGridCell.reason);
var intersectedCell = msGridRangeThingIDunno(originGridCell.value, range);
if (intersectedCell == null) {
Bm(
"calc.runtime.dereference",
ErrorType.VALUE,
"empty intersection, oper = " + l(oper)
);
return valueErrorOper;
}
return msDereferenceIntoCellValue(
funcDispatcher,
sheetGridCell(sheets, intersectedCell),
anchors
);
}
return msDereferenceIntoArray(
funcDispatcher,
sheetGridRange(sheets, range),
anchors
);
}
if (0 != (targetType & OperKind.Array)) {
// Array bit is set
return msDereferenceIntoArray(
funcDispatcher,
sheetGridRange(sheets, range),
anchors
);
}
Bm(
"calc.runtime.dereference",
ErrorType.VALUE,
"bad target type " + targetType
);
return valueErrorOper;
}

You can find an appendix containing the definitions of msDereferenceIntoArray and msDereferenceIntoCellValue here.

Part 1 of this function creates a bitmask of all function types that appear in the given function arguments, but not in the corresponding signature type. In the happy path where all function arguments are valid, we call FunctionDispatcher.applyOperatorOrFunction and return.

Otherwise, if there are some arguments whose type doesn’t match the signature, we do one of two things:

If OperKind.Array is one of the given types which don’t match the signature, we return an array whose elements are the result of FunctionDispatcher.dispatchFunction called recursively. This is similar to what Apiary does when the first element of this signature is AUX (see more below).

Otherwise, we try to coerce the given argument values to fit the signature types (see more below). If any of the given argument values is an error, and the corresponsing signature type doesn’t have the OperKind.Error bit set, we bubble the error up the chain by returning. Also, if a 3D range is given to a function which doesn’t have the FunctionFlags.Allow3dRef flag set, we return a #VALUE! error. This is in part 3.

FunctionDispatcher.prototype.dispatchFunction = function (
funcId,
funcSignature,
isFormulaRoot,
funcArgValues
) {
// Throw error if the number of arguments is not valid
if (!isValidArgsLength(funcSignature, funcArgValues.length)) {
var i = JSON.stringify(funcSignature);
var o =
"calc.runtime.FunctionDispatcher.dispatchFunction: " +
(errorMsg =
"bad application of " +
JSON.stringify(funcId) +
" to " +
funcArgValues.length +
" arguments, ftype = " +
i);
return ulsLogger.ULS.assertTag(
593372499,
u.LogCategory.msoulscat_ES_EWAJS,
!1,
o
);
}
/* Part 1 */
var misfitTypes = SignatureType.None;
for (var funcArgIdx = 0; funcArgIdx < funcArgValues.length; funcArgIdx += 1) {
var funcArg = funcArgValues[funcArgIdx];
if (
0 ==
(funcArg.kind &
getArgType(funcSignature, funcArgIdx, funcArgValues.length))
) {
// The given type is not one of the ones listed in the signature.
misfitTypes |= funcArg.kind;
}
if (
isRangeOperKind(funcArg) &&
0 == (funcSignature.flags & FunctionFlags.Allow3dRef) &&
!isSheetIndex(funcArg.sheets)
) {
// A 3D reference was given even though it's not allowed given the flags
misfitTypes |= funcArg.kind; // |= OperKind.Range
}
}
if (misfitTypes === SignatureType.None) {
return this.applyOperatorOrFunction(funcId, funcArgValues, isFormulaRoot);
}
/* Part 2 */
if (0 != (misfitTypes & OperKind.Array)) {
// An array was given even though it was not supported, so we map
// over the array with the function and call it a day.
return msMachineCallWhichMapsFuncOverArray(
this,
funcSignature,
function (funcArgValues) {
return this.handleLambda(
this.dispatchFunction(funcId, funcSignature, false, funcArgValues)
);
},
funcArgValues
);
}
/* Part 3 */
// This statement is probably useless given the if condition above.
misfitTypes &= ~OperKind.Array;
var coercedArgValues = [];
for (funcArgIdx = 0; funcArgIdx < funcArgValues.length; funcArgIdx += 1) {
var funcArgValue = funcArgValues[funcArgIdx];
if (0 != (misfitTypes & funcArgValue.kind)) {
var targetType = getArgType(
funcSignature,
funcArgIdx,
funcArgValues.length
);
funcArgValue = this.coerceOper(funcArgValue, targetType);
if (isErrorOperKind(funcArgValue) && 0 == (targetType & OperKind.Error)) {
// If the value we have is an error, and the signature says
// the function doesn't want errors, we bubble the error.
return funcArgValue;
}
if (
isRangeOperKind(funcArgValue) &&
0 == (funcSignature.flags & FunctionFlags.Allow3dRef) &&
!isSheetIndex(funcArgValue.sheets)
) {
// A 3D reference was given even though it's not allowed given the flags.
var errorMsg = "3D reference, oper = " + l(funcArgValue);
return (
Bm(
"calc.runtime.FunctionDispatcher.dispatchFunction",
ErrorType.VALUE,
errorMsg
),
valueErrorOper
);
}
}
coercedArgValues[funcArgIdx] = funcArgValue;
}
return this.applyOperatorOrFunction(funcId, coercedArgValues, isFormulaRoot);
};

The array mapping behaviour is implemented in msMachineCallWhichMapsFuncOverArray (sorry, I couldn’t figure out a better name):

function msMachineCallWhichMapsFuncOverArray(
funcDispatcher,
signature,
transformationFunc,
funcArgs
) {
// Determine the size of the matrix we should return.
var maxNumRows = 0;
var maxNumCols = 0;
for (var idx = 0; idx < funcArgs.length; idx += 1) {
var funcArg = funcArgs[idx];
if (
isArrayOperKind(funcArg) &&
0 == (getArgType(signature, idx, funcArgs.length) & OperKind.Array)
) {
maxNumRows = fastMax(maxNumRows, funcArg.rows);
maxNumCols = fastMax(maxNumCols, funcArg.cols);
} else {
maxNumRows = fastMax(maxNumRows, 1);
maxNumCols = fastMax(maxNumCols, 1);
}
}
// Turn the list of function argument values into matrices of the size we
// calculated above. All matrix elements are the function argument value.
var funcArgArrays = funcArgs.map(function (funcArg, funcArgIdx, funcArgs) {
return msCreateNonstrictArrayForFuncArg(
funcDispatcher.mathpack,
maxNumRows,
maxNumCols,
funcArg,
getArgType(signature, funcArgIdx, funcArgs.length)
);
});
// This is where the magic happens. We create a "machine call" to an
// "array machine" which ultimately evaluates to a matrix of size
// `maxNumRows * maxNumCols` which is the result of the function handler
// being called to produce each element of the matrix.
//
// Those are mostly just Microsoft implementation details for the matrix
// mapping behaviour Apiary implements in `mxOps` in `evaluate-common.ts`.
return machineCall(
new ArrayMachine(funcDispatcher, maxNumRows, maxNumCols, function (
row,
col
) {
return transformationFunc(
// Gets one cell from each argument matrix: [1, -2, 3, 0, ...]
funcArgArrays.map(function (nonstrictArr) {
return nonstrictArr.get(row, col);
})
);
})
);
}

The internals of msCreateNonstrictArrayForFuncArg are below

function msCreateNonstrictArray2DFilledWithValue(
numRows,
numColumns,
fillValue
) {
return new NonstrictArray2D(numRows, numColumns, function (e, t) {
return fillValue;
});
}
function msCreateNonstrictArrayForFuncArg(
mathpack,
numRows,
numCols,
node,
targetType
) {
switch (node.kind) {
case OperKind.Missing:
case OperKind.Blank:
return msCreateNonstrictArray2DFilledWithValue(
numRows,
numCols,
mathpack.zero
);
case OperKind.Array:
return 0 != (targetType & OperKind.Array)
? msCreateNonstrictArray2DFilledWithValue(numRows, numCols, node)
: (function (numRows, numCols, strictArr, errVal) {
return strictArr.rows === numRows && strictArr.cols === numCols
? strictArr
: 1 === strictArr.rows && 1 === strictArr.cols
? msCreateNonstrictArray2DFilledWithValue(
numRows,
numCols,
strictArr.get(0, 0)
)
: 1 === strictArr.rows
? new NonstrictArray2D(numRows, numCols, function (row, col) {
return col < strictArr.cols ? strictArr.get(0, col) : errVal;
})
: 1 === strictArr.cols
? new NonstrictArray2D(numRows, numCols, function (row, col) {
return row < strictArr.rows ? strictArr.get(row, 0) : errVal;
})
: new NonstrictArray2D(numRows, numCols, function (row, col) {
return row < strictArr.rows && col < strictArr.cols
? strictArr.get(row, col)
: errVal;
});
})(
numRows,
numCols,
new StrictArray2D(node.rows, node.cols, node.opers),
naErrorOper
);
case OperKind.Range:
default:
return msCreateNonstrictArray2DFilledWithValue(numRows, numCols, node);
}
}

For context, StrictArray2D and NonstrictArray2D differ in that nonstrict arrays are lazy, and strict arrays are not. The third argument to the NonstrictArray2D constructor is a function which takes two arguments, one for the row number, and one for the col number. The function should lazily resolve to a value for those given coordinates.

The following subroutine is the one that Excel uses to coerce a value to a target type. It has been deobfuscated and commented for easier legibility.

If the target contains more than one type, the algorithm used seems to be:

  1. If the given value’s type is a subtype of the target type, return the value unchanged.
  2. If the target type contains the number type, try to convert the given value to a number.
  3. If the target type contains the string type, try to convert the given value to a string.
  4. If the target type contains the boolean type, try to convert the given value to a boolean.
  5. If the target type contains the array type, return an array containing only the given value.
function msDefaultValueForType(e, targetType) {
if (0 != (targetType & OperKind.Number)) {
return mathpack.zero;
}
if (0 != (targetType & OperKind.String)) {
return msEmptyString;
}
if (0 != (targetType & OperKind.Boolean)) {
return falseOper;
}
return valueErrorOper;
}
function coerceOper(funcDispatcher, funcArgValue, targetType) {
var result = (function coerceOperInner(
funcDispatcher,
funcArgValue,
targetType
) {
var cfg = funcDispatcher.globals.config;
var localeInfo = cfg.localeInfo;
var mathpack = cfg.mathpack;
if (isNumberOperKind(funcArgValue)) {
if (0 != (targetType & OperKind.Number)) {
// The target accepts numbers, so there's no conversion needed
return funcArgValue;
}
if (0 != (targetType & OperKind.String)) {
// Format the number as a string
return stringOper(mathpack.formatGeneral(localeInfo, funcArgValue, 20));
}
if (0 != (targetType & OperKind.Boolean)) {
// Turn number into a boolean
return booleanOper(0 !== mathpack.cond(funcArgValue));
}
if (0 != (targetType & OperKind.Array)) {
// Turn the number into an array with a single value
return arrayOper(1, 1, [[funcArgValue]]);
}
return valueErrorOper;
}
if (isStringOperKind(funcArgValue)) {
if (0 != (targetType & OperKind.String)) {
// The target accepts strings; no conversion is needed.
return funcArgValue;
}
if (0 != (targetType & OperKind.Number)) {
// Try turning string into a number
if (0 === funcArgValue.value.length) {
// Empty string is not a valid number
return valueErrorOper;
}
if (funcArgValue.value.length > 255) {
// Number is too large to parse, I guess?
return valueErrorOper;
}
var converted = funcDispatcher.numberFormatter.parseGeneral(
funcArgValue.value,
false,
true,
GetTimeKind.NonVolatile
);
if (!isSuccess(converted)) {
// String couldn't be converted to a number
msThrowEvaluationError(converted.reason);
}
if (converted.value !== undefined) {
return converted.value.value;
}
return valueErrorOper;
}
if (0 != (targetType & OperKind.Boolean)) {
if (
0 ===
localeInfo.stringCompareCI(funcArgValue.value, localeInfo.trueName)
) {
// Convert the string "TRUE" to TRUE
return trueOper;
}
if (
0 ===
localeInfo.stringCompareCI(funcArgValue.value, localeInfo.falseName)
) {
// Convert the string "FALSE" to FALSE
return falseOper;
}
return valueErrorOper;
}
if (0 != (targetType & OperKind.Array)) {
// Turn the string into an array with a single value
return arrayOper(1, 1, [[funcArgValue]]);
}
return valueErrorOper;
}
if (isBooleanOperKind(funcArgValue)) {
if (0 != (targetType & OperKind.Boolean)) {
// The target accepts booleans; no conversion is needed.
return funcArgValue;
}
if (0 != (targetType & OperKind.Number)) {
if (funcArgValue.value) {
// Convert TRUE into 1
return mathpack.one;
} else {
// Convert FALSE into 0
return mathpack.zero;
}
}
if (0 != (targetType & OperKind.String)) {
if (funcArgValue.value) {
// Convert TRUE into "TRUE"
return stringOper(localeInfo.trueName);
} else {
// Convert FALSE into FALSE
return stringOper(localeInfo.falseName);
}
}
if (0 != (targetType & OperKind.Array)) {
// Turn the boolean into an array with a single value
return arrayOper(1, 1, [[funcArgValue]]);
}
return valueErrorOper;
}
if (0 != (targetType & funcArgValue.kind)) {
return funcArgValue;
}
switch (funcArgValue.kind) {
case OperKind.Error:
return funcArgValue;
case OperKind.Missing:
return 0 != (targetType & OperKind.Blank)
? blankOper
: msDefaultValueForType(mathpack, targetType);
case OperKind.Blank:
return msDefaultValueForType(mathpack, targetType);
case OperKind.Rich:
return coerceOperInner(
funcDispatcher,
funcArgValue.getFallback(),
targetType
);
default:
return valueErrorOper;
}
})(funcDispatcher, funcArgValue, targetType);
if (isErrorOperKind(result) && !isErrorOperKind(funcArgValue)) {
// This is basically a debug-print that is sent to Microsoft but is not
// displayed to the user.
msWriteTraceVerbose(
"calc.runtime.coerceOper",
result.type,
"kind = " + funcArgValue.kind
)
}
return result;
}