Excel under the hood
The following has been learned by reverse engineering Office 365 Excel Online.
Function signatures
Section titled “Function signatures”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.
Invariants
Section titled “Invariants”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.
Routine: Validate the number of arguments
Section titled “Routine: Validate the number of arguments”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]; }}Example: Switch
Section titled “Example: Switch”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])| Argument | Description |
|---|---|
| expression (required) | Expression is the value (such as a number, date or some text) that will be compared against value1…value126. |
| value1…value126 | ValueN is a value that will be compared against expression. |
| result1…result126 | ResultN 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 function type
Section titled “The function type”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 ofOperKindbitfields, one for each head type.restTypes: A list ofOperKindbitfields, one for each rest type.tailTypes: A list ofOperKindbitfields, one for each tail type.returnType: A singleOperKindbitfield describing the return type of the function.staticPrecs: “Special precedents” which are set statically rather than dynamically. A singleSpecialPrecsbitfield. 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 singleFunctionFlagsbitfield.
Bitfields
Section titled “Bitfields”The OperKind bitfield is as follows:
| Bit | Name | Comment |
|---|---|---|
| 1 | Number | |
| 2 | String | |
| 4 | Boolean | |
| 8 | Error | |
| 16 | Range | |
| 32 | Array | |
| 64 | Missing | The value of an argument which omitted (e.g. SUM(1, , 3, , 5))… maybe? |
| 128 | Blank | The value of a cell which is blank… probably |
| 256 | Rich | Rich 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) | Name | Comment |
|---|---|---|
| 0 | None | No bits are set |
| 512 | Goto | I don’t know what this is |
| 1024 | Ref3D | 3D reference |
| 2047 | All | All bits are set |
The SpecialPrecs bitfields is as follows:
| Bit(s) | Name | Comment |
|---|---|---|
| 0 | None | No bits are set |
| 1 | OwnDoc | Doesn’t seem to be used, AFAICT |
| 2 | OwnSheet | Doesn’t seem to be used, AFAICT |
| 4 | OwnRow | Set for ROW function |
| 8 | OwnCol | Set for COL function |
| 16 | OwnRowCopy | Doesn’t seem to be used, AFAICT |
| 32 | OwnColCopy | Doesn’t seem to be used, AFAICT |
| 64 | OwnDocSheets | Set for SHEET, SHEETS functions |
| 128 | CellProps | |
| 256 | AllCells | Set for OFFSET, INDIRECT, SUMIF, AVERAGEIF, LOOKUP, CELL |
| 512 | AllNames | Doesn’t seem to be used, AFAICT |
| 1024 | Time | Enabled if the function requests the current time |
| 2048 | Calc | Enabled if the function requests randomness |
| 4096 | InFilterMode | Only set dynamically (not statically as a part of function signatures it seems) |
| 8192 | RefStyle | |
| 16383 | All | All bits are set |
The FunctionFlags bitfield is as follows:
| Bit(s) | Name | Comment |
|---|---|---|
| 0 | None | |
| 1 | Dark | Set for a lot of functions. I have no idea what it means and it doesn’t seem to be used. |
| 2 | Macro | |
| 4 | OnlyParsedDuringLoad | |
| 8 | Internal | |
| 16 | Allow3dRef | Allows 3d references |
Function and operator calls
Section titled “Function and operator calls”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:
- The function ID, an integer in the range
[0, 681]. - The function’s signature.
- The arguments with which you want to evaluate the function.
The procedure is as follows:
- You call
dereferenceOpersto dereference any function arguments which have the typeOperKind.Rangebut whose corresponding signature type doesn’t have theOperKind.Rangebit set. - You create an instance of the
FunctionDispatcherclass, and call itsdispatchFunctionmethod. - You may need to call
FunctionDispatcher.handleLambdaon the result ofdispatchFunction, 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
Section titled “Dereferencing”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:
dereferencetakes the target type into account. If the target type containsOperKind.Rangeas 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.Arrayas 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.
Inside of dispatchFunction
Section titled “Inside of dispatchFunction”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);};Array mapping behaviour
Section titled “Array mapping behaviour”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.
Type coercion
Section titled “Type coercion”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:
- If the given value’s type is a subtype of the target type, return the value unchanged.
- If the target type contains the number type, try to convert the given value to a number.
- If the target type contains the string type, try to convert the given value to a string.
- If the target type contains the boolean type, try to convert the given value to a boolean.
- 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;}