Skip to content

Commit acd036b

Browse files
authored
@vararg directive (#630)
* preventing pack/unpack of rest parameters referenced with spread operators * fixed referencing rest parameters in nested functions * moved some general logic into a helper * @elipsisForward directive to replace implicit optimization * fixed reference tracking when noHoisting is set * Allowing @elipsisForward to take no args so it can be used to access global elipsis * updated to use @vararg type instead of @elipsisForward function * removing unused error * Changed case in vararg
1 parent a940c71 commit acd036b

File tree

6 files changed

+167
-13
lines changed

6 files changed

+167
-13
lines changed

src/Decorator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export class Decorator {
2929
return DecoratorKind.NoSelf;
3030
case "noselfinfile":
3131
return DecoratorKind.NoSelfInFile;
32+
case "vararg":
33+
return DecoratorKind.Vararg;
3234
case "forrange":
3335
return DecoratorKind.ForRange;
3436
}
@@ -63,5 +65,6 @@ export enum DecoratorKind {
6365
LuaTable = "LuaTable",
6466
NoSelf = "NoSelf",
6567
NoSelfInFile = "NoSelfInFile",
68+
Vararg = "Vararg",
6669
ForRange = "ForRange",
6770
}

src/LuaTransformer.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ interface SymbolInfo {
2525
}
2626

2727
interface FunctionDefinitionInfo {
28-
referencedSymbols: Set<tstl.SymbolId>;
28+
referencedSymbols: Map<tstl.SymbolId, ts.Node[]>;
2929
definition?: tstl.VariableDeclarationStatement | tstl.AssignmentStatement;
3030
}
3131

3232
interface Scope {
3333
type: ScopeType;
3434
id: number;
35-
referencedSymbols?: Set<tstl.SymbolId>;
35+
referencedSymbols?: Map<tstl.SymbolId, ts.Node[]>;
3636
variableDeclarations?: tstl.VariableDeclarationStatement[];
3737
functionDefinitions?: Map<tstl.SymbolId, FunctionDefinitionInfo>;
3838
importStatements?: tstl.Statement[];
@@ -1388,12 +1388,31 @@ export class LuaTransformer {
13881388
return [paramNames, dotsLiteral, restParamName];
13891389
}
13901390

1391+
protected isRestParameterReferenced(identifier: tstl.Identifier, scope: Scope): boolean {
1392+
if (!identifier.symbolId) {
1393+
return true;
1394+
}
1395+
if (scope.referencedSymbols === undefined) {
1396+
return false;
1397+
}
1398+
const references = scope.referencedSymbols.get(identifier.symbolId);
1399+
if (!references) {
1400+
return false;
1401+
}
1402+
// Ignore references to @vararg types in spread elements
1403+
return references.some(
1404+
r => !r.parent || !ts.isSpreadElement(r.parent) || !tsHelper.isVarArgType(r, this.checker)
1405+
);
1406+
}
1407+
13911408
protected transformFunctionBody(
13921409
parameters: ts.NodeArray<ts.ParameterDeclaration>,
13931410
body: ts.Block,
13941411
spreadIdentifier?: tstl.Identifier
13951412
): [tstl.Statement[], Scope] {
13961413
this.pushScope(ScopeType.Function);
1414+
const bodyStatements = this.performHoisting(this.transformStatements(body.statements));
1415+
const scope = this.popScope();
13971416

13981417
const headerStatements = [];
13991418

@@ -1426,18 +1445,14 @@ export class LuaTransformer {
14261445
}
14271446

14281447
// Push spread operator here
1429-
if (spreadIdentifier) {
1448+
if (spreadIdentifier && this.isRestParameterReferenced(spreadIdentifier, scope)) {
14301449
const spreadTable = this.wrapInTable(tstl.createDotsLiteral());
14311450
headerStatements.push(tstl.createVariableDeclarationStatement(spreadIdentifier, spreadTable));
14321451
}
14331452

14341453
// Binding pattern statements need to be after spread table is declared
14351454
headerStatements.push(...bindingPatternDeclarations);
14361455

1437-
const bodyStatements = this.performHoisting(this.transformStatements(body.statements));
1438-
1439-
const scope = this.popScope();
1440-
14411456
return [headerStatements.concat(bodyStatements), scope];
14421457
}
14431458

@@ -1844,7 +1859,7 @@ export class LuaTransformer {
18441859
if (!scope.functionDefinitions) {
18451860
scope.functionDefinitions = new Map();
18461861
}
1847-
const functionInfo = { referencedSymbols: functionScope.referencedSymbols || new Set() };
1862+
const functionInfo = { referencedSymbols: functionScope.referencedSymbols || new Map() };
18481863
scope.functionDefinitions.set(name.symbolId, functionInfo);
18491864
}
18501865
return this.createLocalOrExportedOrGlobalDeclaration(name, functionExpression, functionDeclaration);
@@ -4603,6 +4618,10 @@ export class LuaTransformer {
46034618
return innerExpression;
46044619
}
46054620

4621+
if (ts.isIdentifier(expression.expression) && tsHelper.isVarArgType(expression.expression, this.checker)) {
4622+
return tstl.createDotsLiteral(expression);
4623+
}
4624+
46064625
const type = this.checker.getTypeAtLocation(expression.expression);
46074626
if (tsHelper.isArrayType(type, this.checker, this.program)) {
46084627
return this.createUnpackCall(innerExpression, expression);
@@ -5278,13 +5297,20 @@ export class LuaTransformer {
52785297
if (declaration && identifier.pos < declaration.pos) {
52795298
throw TSTLErrors.ReferencedBeforeDeclaration(identifier);
52805299
}
5281-
} else if (symbolId !== undefined) {
5300+
}
5301+
5302+
if (symbolId !== undefined) {
52825303
//Mark symbol as seen in all current scopes
52835304
for (const scope of this.scopeStack) {
52845305
if (!scope.referencedSymbols) {
5285-
scope.referencedSymbols = new Set();
5306+
scope.referencedSymbols = new Map();
5307+
}
5308+
let references = scope.referencedSymbols.get(symbolId);
5309+
if (!references) {
5310+
references = [];
5311+
scope.referencedSymbols.set(symbolId, references);
52865312
}
5287-
scope.referencedSymbols.add(symbolId);
5313+
references.push(identifier);
52885314
}
52895315
}
52905316
}

src/TSHelper.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ export class TSHelper {
184184
return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.LuaIterator);
185185
}
186186

187+
public static isRestParameter(node: ts.Node, checker: ts.TypeChecker): boolean {
188+
const symbol = checker.getSymbolAtLocation(node);
189+
if (!symbol) {
190+
return false;
191+
}
192+
const declarations = symbol.getDeclarations();
193+
if (!declarations) {
194+
return false;
195+
}
196+
return declarations.some(d => ts.isParameter(d) && d.dotDotDotToken !== undefined);
197+
}
198+
199+
public static isVarArgType(node: ts.Node, checker: ts.TypeChecker): boolean {
200+
const type = checker.getTypeAtLocation(node);
201+
return type !== undefined && TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.Vararg);
202+
}
203+
187204
public static isForRangeType(node: ts.Node, checker: ts.TypeChecker): boolean {
188205
const type = checker.getTypeAtLocation(node);
189206
return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.ForRange);

test/translation/__snapshots__/transformation.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ end"
256256
exports[`Transformation (functionRestArguments) 1`] = `
257257
"function varargsFunction(self, a, ...)
258258
local b = ({...})
259+
local c = b
259260
end"
260261
`;
261262

@@ -319,7 +320,6 @@ end
319320
function MyClass.prototype.____constructor(self)
320321
end
321322
function MyClass.prototype.varargsFunction(self, a, ...)
322-
local b = ({...})
323323
end"
324324
`;
325325

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
function varargsFunction(a: string, ...b: string[]): void {}
1+
function varargsFunction(a: string, ...b: string[]): void {
2+
const c = b;
3+
}

test/unit/functions.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,109 @@ test("Function rest binding pattern", () => {
504504

505505
expect(result).toBe("defxyzabc");
506506
});
507+
508+
test.each([{}, { noHoisting: true }])("Function rest parameter", compilerOptions => {
509+
const code = `
510+
function foo(a: unknown, ...b: string[]) {
511+
return b.join("");
512+
}
513+
return foo("A", "B", "C", "D");
514+
`;
515+
516+
expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD");
517+
});
518+
519+
test.each([{}, { noHoisting: true }])("Function nested rest parameter", compilerOptions => {
520+
const code = `
521+
function foo(a: unknown, ...b: string[]) {
522+
function bar() {
523+
return b.join("");
524+
}
525+
return bar();
526+
}
527+
return foo("A", "B", "C", "D");
528+
`;
529+
530+
expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD");
531+
});
532+
533+
test.each([{}, { noHoisting: true }])("Function nested rest spread", compilerOptions => {
534+
const code = `
535+
function foo(a: unknown, ...b: string[]) {
536+
function bar() {
537+
const c = [...b];
538+
return c.join("");
539+
}
540+
return bar();
541+
}
542+
return foo("A", "B", "C", "D");
543+
`;
544+
545+
expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD");
546+
});
547+
548+
test.each([{}, { noHoisting: true }])("Function rest parameter (unreferenced)", compilerOptions => {
549+
const code = `
550+
function foo(a: unknown, ...b: string[]) {
551+
return "foobar";
552+
}
553+
return foo("A", "B", "C", "D");
554+
`;
555+
556+
expect(util.transpileString(code, compilerOptions)).not.toMatch("b = ({...})");
557+
expect(util.transpileAndExecute(code, compilerOptions)).toBe("foobar");
558+
});
559+
560+
test.each([{}, { noHoisting: true }])("@vararg", compilerOptions => {
561+
const code = `
562+
/** @vararg */ type LuaVarArg<A extends unknown[]> = A & { __luaVarArg?: never };
563+
function foo(a: unknown, ...b: LuaVarArg<unknown[]>) {
564+
const c = [...b];
565+
return c.join("");
566+
}
567+
function bar(a: unknown, ...b: LuaVarArg<unknown[]>) {
568+
return foo(a, ...b);
569+
}
570+
return bar("A", "B", "C", "D");
571+
`;
572+
573+
const lua = util.transpileString(code, compilerOptions);
574+
expect(lua).not.toMatch("b = ({...})");
575+
expect(lua).not.toMatch("unpack");
576+
expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD");
577+
});
578+
579+
test.each([{}, { noHoisting: true }])("@vararg array access", compilerOptions => {
580+
const code = `
581+
/** @vararg */ type LuaVarArg<A extends unknown[]> = A & { __luaVarArg?: never };
582+
function foo(a: unknown, ...b: LuaVarArg<unknown[]>) {
583+
const c = [...b];
584+
return c.join("") + b[0];
585+
}
586+
return foo("A", "B", "C", "D");
587+
`;
588+
589+
expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCDB");
590+
});
591+
592+
test.each([{}, { noHoisting: true }])("@vararg global", compilerOptions => {
593+
const code = `
594+
/** @vararg */ type LuaVarArg<A extends unknown[]> = A & { __luaVarArg?: never };
595+
declare const arg: LuaVarArg<string[]>;
596+
const arr = [...arg];
597+
const result = arr.join("");
598+
`;
599+
600+
const luaBody = util.transpileString(code, compilerOptions, false);
601+
expect(luaBody).not.toMatch("unpack");
602+
603+
const lua = `
604+
function test(...)
605+
${luaBody}
606+
return result
607+
end
608+
return test("A", "B", "C", "D")
609+
`;
610+
611+
expect(util.executeLua(lua)).toBe("ABCD");
612+
});

0 commit comments

Comments
 (0)