Skip to content

Commit 4bd2d56

Browse files
committed
Fix: distinguish syntactically empty args from empty cell references in emptyAsDefault
The emptyAsDefault check in coerceArgumentsToRequiredTypes used rawArg === EmptyValue to detect empty arguments. However, both syntactically empty arguments (=ADDRESS(1,1,)) and references to empty cells (=ADDRESS(1,1,A1) where A1 is blank) resolve to the same EmptyValue singleton. This caused empty cell references to incorrectly use the default value instead of being coerced to 0 (which would then fail validation). Fix: track which argument positions correspond to AstNodeType.EMPTY nodes (i.e., syntactically omitted arguments) and use that flag instead of checking for EmptyValue when deciding whether to apply emptyAsDefault.
1 parent c456e93 commit 4bd2d56

1 file changed

Lines changed: 33 additions & 4 deletions

File tree

‎src/interpreter/plugin/FunctionPlugin.ts‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,29 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck<Function
312312
return ret
313313
}
314314

315+
/**
316+
* Builds syntactically-empty flags aligned with the expanded evaluated arguments.
317+
* When ranges are expanded, a single AST node may produce multiple evaluated values;
318+
* only nodes with `AstNodeType.EMPTY` are considered syntactically empty.
319+
*/
320+
private buildSyntacticallyEmptyFlagsForExpandedArgs(args: Ast[], evaluatedArguments: [InterpreterValue, boolean][]): boolean[] {
321+
const flags: boolean[] = []
322+
let evalIdx = 0
323+
for (const ast of args) {
324+
const isEmpty = ast.type === AstNodeType.EMPTY
325+
if (evalIdx < evaluatedArguments.length && evaluatedArguments[evalIdx][1]) {
326+
while (evalIdx < evaluatedArguments.length && evaluatedArguments[evalIdx][1]) {
327+
flags.push(isEmpty)
328+
evalIdx++
329+
}
330+
} else {
331+
flags.push(isEmpty)
332+
evalIdx++
333+
}
334+
}
335+
return flags
336+
}
337+
315338
protected coerceScalarToNumberOrError = (arg: InternalScalarValue): ExtendedNumber | CellError => this.arithmeticHelper.coerceScalarToNumberOrError(arg)
316339

317340
protected coerceToType(arg: InterpreterValue, coercedType: FunctionArgument, state: InterpreterState): Maybe<InterpreterValue | complex | RawNoErrorScalarValue> {
@@ -410,6 +433,9 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck<Function
410433
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
411434
const argumentValues: InterpreterValue[] = evaluatedArguments.map(([value, _]: [InterpreterValue, boolean]) => value as InterpreterValue)
412435
const argumentIgnorableFlags = evaluatedArguments.map(([_, ignorable]) => ignorable)
436+
const syntacticallyEmptyFlags = metadata.expandRanges
437+
? this.buildSyntacticallyEmptyFlagsForExpandedArgs(args, evaluatedArguments)
438+
: args.map((ast) => ast.type === AstNodeType.EMPTY)
413439
const argumentMetadata = this.buildMetadataForEachArgumentValue(argumentValues.length, metadata)
414440
const isVectorizationOn = state.arraysFlag && !metadata.vectorizationForbidden
415441

@@ -421,13 +447,13 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck<Function
421447

422448
if (resultArrayHeight === 1 && resultArrayWidth === 1) {
423449
const vectorizedArguments = this.vectorizeAndBroadcastArgumentsIfNecessary(isVectorizationOn, argumentValues, argumentMetadata, 0, 0)
424-
return this.calculateSingleCellOfResultArray(state, vectorizedArguments, argumentMetadata, argumentIgnorableFlags, functionImplementation, metadata.returnNumberType)
450+
return this.calculateSingleCellOfResultArray(state, vectorizedArguments, argumentMetadata, argumentIgnorableFlags, syntacticallyEmptyFlags, functionImplementation, metadata.returnNumberType)
425451
}
426452

427453
const resultArray: InternalScalarValue[][] = [ ...Array(resultArrayHeight).keys() ].map(row =>
428454
[ ...Array(resultArrayWidth).keys() ].map(col => {
429455
const vectorizedArguments = this.vectorizeAndBroadcastArgumentsIfNecessary(isVectorizationOn, argumentValues, argumentMetadata, row, col)
430-
const result = this.calculateSingleCellOfResultArray(state, vectorizedArguments, argumentMetadata, argumentIgnorableFlags, functionImplementation, metadata.returnNumberType)
456+
const result = this.calculateSingleCellOfResultArray(state, vectorizedArguments, argumentMetadata, argumentIgnorableFlags, syntacticallyEmptyFlags, functionImplementation, metadata.returnNumberType)
431457

432458
if (result instanceof SimpleRangeValue) {
433459
throw new Error('Function returning array cannot be vectorized.')
@@ -445,10 +471,11 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck<Function
445471
vectorizedArguments: Maybe<InterpreterValue>[],
446472
argumentsMetadata: FunctionArgument[],
447473
argumentIgnorableFlags: boolean[],
474+
syntacticallyEmptyFlags: boolean[],
448475
functionImplementation: (...arg: any) => InterpreterValue,
449476
returnNumberType: NumberType | undefined,
450477
): RawInterpreterValue {
451-
const coercedArguments = this.coerceArgumentsToRequiredTypes(state, vectorizedArguments, argumentsMetadata, argumentIgnorableFlags)
478+
const coercedArguments = this.coerceArgumentsToRequiredTypes(state, vectorizedArguments, argumentsMetadata, argumentIgnorableFlags, syntacticallyEmptyFlags)
452479

453480
if (coercedArguments instanceof CellError) {
454481
return coercedArguments
@@ -463,15 +490,17 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck<Function
463490
vectorizedArguments: Maybe<InterpreterValue>[],
464491
argumentsMetadata: FunctionArgument[],
465492
argumentIgnorableFlags: boolean[],
493+
syntacticallyEmptyFlags: boolean[] = [],
466494
): CellError | Maybe<InterpreterValue | complex | RawNoErrorScalarValue>[] {
467495
const coercedArguments: Maybe<InterpreterValue | complex | RawNoErrorScalarValue>[] = []
468496

469497
for (let i = 0; i < argumentsMetadata.length; i++) {
470498
const argumentMetadata = argumentsMetadata[i]
471499
const rawArg = vectorizedArguments[i]
500+
const isSyntacticallyEmpty = !!syntacticallyEmptyFlags[i]
472501
const argumentValue = rawArg === undefined
473502
? argumentMetadata?.defaultValue
474-
: (rawArg === EmptyValue && argumentMetadata?.emptyAsDefault && argumentMetadata?.defaultValue !== undefined)
503+
: (isSyntacticallyEmpty && argumentMetadata?.emptyAsDefault && argumentMetadata?.defaultValue !== undefined)
475504
? argumentMetadata.defaultValue
476505
: rawArg
477506

0 commit comments

Comments
 (0)