Skip to content

Commit

Permalink
feat: Support rest params in function calls (#2905)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattjohnsonpint authored Feb 7, 2025
1 parent cdd5e01 commit 6e151f8
Show file tree
Hide file tree
Showing 11 changed files with 11,397 additions and 27 deletions.
55 changes: 44 additions & 11 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ import {
findDecorator,
isTypeOmitted,
Source,
TypeDeclaration
TypeDeclaration,
ParameterKind
} from "./ast";

import {
Expand Down Expand Up @@ -6171,16 +6172,7 @@ export class Compiler extends DiagnosticEmitter {
return false;
}

// not yet implemented (TODO: maybe some sort of an unmanaged/lightweight array?)
let hasRest = signature.hasRest;
if (hasRest) {
this.error(
DiagnosticCode.Not_implemented_0,
reportNode.range, "Rest parameters"
);
return false;
}

let minimum = signature.requiredParameters;
let maximum = signature.parameterTypes.length;

Expand Down Expand Up @@ -6225,6 +6217,37 @@ export class Compiler extends DiagnosticEmitter {
}
}

private adjustArgumentsForRestParams(
argumentExpressions: Expression[],
signature: Signature,
reportNode: Node
) : Expression[] {

// if no rest args, return the original args
if (!signature.hasRest) {
return argumentExpressions;
}

// if there are fewer args than params, then the rest args were not provided
// so return the original args
const numArguments = argumentExpressions.length;
const numParams = signature.parameterTypes.length;
if (numArguments < numParams) {
return argumentExpressions;
}

// make an array literal expression from the rest args
let elements = argumentExpressions.slice(numParams - 1);
let range = new Range(elements[0].range.start, elements[elements.length - 1].range.end);
range.source = reportNode.range.source;
let arrExpr = new ArrayLiteralExpression(elements, range);

// return the original args, but replace the rest args with the array
const exprs = argumentExpressions.slice(0, numParams - 1);
exprs.push(arrExpr);
return exprs;
}

/** Compiles a direct call to a concrete function. */
compileCallDirect(
instance: Function,
Expand All @@ -6246,6 +6269,9 @@ export class Compiler extends DiagnosticEmitter {
}
if (instance.hasDecorator(DecoratorFlags.Unsafe)) this.checkUnsafe(reportNode);

argumentExpressions = this.adjustArgumentsForRestParams(argumentExpressions, signature, reportNode);
numArguments = argumentExpressions.length;

// handle call on `this` in constructors
let sourceFunction = this.currentFlow.sourceFunction;
if (sourceFunction.is(CommonFlags.Constructor) && reportNode.isAccessOnThis) {
Expand Down Expand Up @@ -6477,7 +6503,11 @@ export class Compiler extends DiagnosticEmitter {
let declaration = originalParameterDeclarations[minArguments + i];
let initializer = declaration.initializer;
let initExpr: ExpressionRef;
if (initializer) {
if (declaration.parameterKind === ParameterKind.Rest) {
const arrExpr = new ArrayLiteralExpression([], declaration.range.atEnd);
initExpr = this.compileArrayLiteral(arrExpr, type, Constraints.ConvExplicit);
initExpr = module.local_set(operandIndex, initExpr, type.isManaged);
} else if (initializer) {
initExpr = this.compileExpression(
initializer,
type,
Expand Down Expand Up @@ -6863,6 +6893,9 @@ export class Compiler extends DiagnosticEmitter {
return this.module.unreachable();
}

argumentExpressions = this.adjustArgumentsForRestParams(argumentExpressions, signature, reportNode);
numArguments = argumentExpressions.length;

let numArgumentsInclThis = thisArg ? numArguments + 1 : numArguments;
let operands = new Array<ExpressionRef>(numArgumentsInclThis);
let index = 0;
Expand Down
48 changes: 36 additions & 12 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,9 @@ export class Resolver extends DiagnosticEmitter {
ctxFlow,
reportMode,
);

if (!resolvedTypeArguments) {
return null;
}
return this.resolveFunction(
prototype,
resolvedTypeArguments,
Expand Down Expand Up @@ -778,16 +780,24 @@ export class Resolver extends DiagnosticEmitter {
let numParameters = parameterNodes.length;

let argumentNodes: Expression[];
let argumentsRange: Range;
switch (node.kind) {
case NodeKind.Call:
argumentNodes = (<CallExpression>node).args;
case NodeKind.Call: {
const expr = node as CallExpression;
argumentNodes = expr.args;
argumentsRange = expr.argumentsRange;
break;
case NodeKind.New:
argumentNodes = (<NewExpression>node).args;
}
case NodeKind.New: {
const expr = node as NewExpression;
argumentNodes = expr.args;
argumentsRange = expr.argumentsRange;
break;
default:
}
default: {
assert(false);
return null;
}
}

let numArguments = argumentNodes.length;
Expand All @@ -802,16 +812,27 @@ export class Resolver extends DiagnosticEmitter {
if (parameterNodes[i].parameterKind == ParameterKind.Optional) {
continue;
}
// missing initializer -> too few arguments
if (reportMode == ReportMode.Report) {
this.error(
DiagnosticCode.Expected_0_arguments_but_got_1,
node.range, numParameters.toString(), numArguments.toString()
);
if (parameterNodes[i].parameterKind == ParameterKind.Rest) {
// rest params are optional, but one element is needed for type inference
this.error(
DiagnosticCode.Type_argument_expected,
argumentsRange.atEnd
);
} else {
// missing initializer -> too few arguments
this.error(
DiagnosticCode.Expected_0_arguments_but_got_1,
node.range, numParameters.toString(), numArguments.toString()
);
}
}
return null;
}
let typeNode = parameterNodes[i].type;
if (parameterNodes[i].parameterKind == ParameterKind.Rest) {
typeNode = (<NamedTypeNode> typeNode).typeArguments![0];
}
if (typeNode.hasGenericComponent(typeParameterNodes)) {
let type = this.resolveExpression(argumentExpression, ctxFlow, Type.auto, ReportMode.Swallow);
if (type) {
Expand Down Expand Up @@ -2921,10 +2942,13 @@ export class Resolver extends DiagnosticEmitter {
let numSignatureParameters = signatureParameters.length;
let parameterTypes = new Array<Type>(numSignatureParameters);
let requiredParameters = 0;
let hasRest = false;
for (let i = 0; i < numSignatureParameters; ++i) {
let parameterDeclaration = signatureParameters[i];
if (parameterDeclaration.parameterKind == ParameterKind.Default) {
requiredParameters = i + 1;
} else if (parameterDeclaration.parameterKind == ParameterKind.Rest) {
hasRest = true;
}
let typeNode = parameterDeclaration.type;
if (isTypeOmitted(typeNode)) {
Expand Down Expand Up @@ -2984,7 +3008,7 @@ export class Resolver extends DiagnosticEmitter {
returnType = type;
}

let signature = Signature.create(this.program, parameterTypes, returnType, thisType, requiredParameters);
let signature = Signature.create(this.program, parameterTypes, returnType, thisType, requiredParameters, hasRest);

let nameInclTypeParameters = prototype.name;
if (instanceKey.length) nameInclTypeParameters += `<${instanceKey}>`;
Expand Down
14 changes: 14 additions & 0 deletions tests/compiler/call-rest-err.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"asc_flags": [
],
"stderr": [
"TS2322: Type '~lib/string/String' is not assignable to type 'i32'.",
"sum('a', 'b')",
"TS2322: Type '~lib/string/String' is not assignable to type 'i32'.",
"sum('a', 'b')",
"TS2322: Type '~lib/string/String' is not assignable to type 'i32'.",
"count(1, 'a')",
"TS1140: Type argument expected.",
"count()"
]
}
15 changes: 15 additions & 0 deletions tests/compiler/call-rest-err.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function sum(...args: i32[]): i32 {
let sum = 0;
for (let i = 0, k = args.length; i < k; ++i) {
sum += args[i];
}
return sum;
}

function count<T>(...args: T[]): i32 {
return args.length;
}

sum('a', 'b'); // expect a type mismatch error on each argument
count(1, 'a'); // expect a type mismatch error on the second argument
count(); // expect type inference error
Loading

0 comments on commit 6e151f8

Please sign in to comment.