X Tutup
Skip to content

Commit 9783568

Browse files
Intermediate SpreadElements fix (#832)
* Use smarter expression spreading * Fix up test to check that concat doesn't flatten args * flattenExpressions -> flattenSpreadExpressions * Use for-of instead of reduce and rename flattenExpressions * Move test cases up to in function call and in array literal * Update src/transformation/visitors/call.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * Update src/transformation/visitors/call.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * Add test case to check concat * Add in array literal/of tuple return call test * Add test for potential edge case * Add in array literal/of array literal /w OmittedExpression * Refactor unneeded check in call * Allow spreadable expressions in any order * Add of string literal mixed * Update test/unit/spread.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * Update test/unit/spread.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * of string literal mixed -> of string literal * spreadCases -> arrayLiteralCases * Update test/unit/spread.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * Add tuple return tests to in function call * Seperate spread to in function and in array literal * Join spread function call and array literal tests Co-authored-by: ark120202 <ark120202@gmail.com>
1 parent 9f61a82 commit 9783568

File tree

3 files changed

+135
-29
lines changed

3 files changed

+135
-29
lines changed

src/transformation/visitors/call.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,78 @@ import { transformLuaTableCallExpression } from "./lua-table";
1515

1616
export type PropertyCallExpression = ts.CallExpression & { expression: ts.PropertyAccessExpression };
1717

18+
function getExpressionsBeforeAndAfterFirstSpread(
19+
expressions: readonly ts.Expression[]
20+
): [readonly ts.Expression[], readonly ts.Expression[]] {
21+
// [a, b, ...c, d, ...e] --> [a, b] and [...c, d, ...e]
22+
const index = expressions.findIndex(ts.isSpreadElement);
23+
const hasSpreadElement = index !== -1;
24+
const before = hasSpreadElement ? expressions.slice(0, index) : expressions;
25+
const after = hasSpreadElement ? expressions.slice(index) : [];
26+
return [before, after];
27+
}
28+
29+
function transformSpreadableExpressionsIntoArrayConcatArguments(
30+
context: TransformationContext,
31+
expressions: readonly ts.Expression[] | ts.NodeArray<ts.Expression>
32+
): lua.Expression[] {
33+
// [...array, a, b, ...tuple()] --> [ [...array], [a, b], [...tuple()] ]
34+
// chunk non-spread arguments together so they don't concat
35+
const chunks: ts.Expression[][] = [];
36+
for (const [index, expression] of expressions.entries()) {
37+
if (ts.isSpreadElement(expression)) {
38+
chunks.push([expression]);
39+
const next = expressions[index + 1];
40+
if (next && !ts.isSpreadElement(next)) {
41+
chunks.push([]);
42+
}
43+
} else {
44+
let lastChunk = chunks[chunks.length - 1];
45+
if (!lastChunk) {
46+
lastChunk = [];
47+
chunks.push(lastChunk);
48+
}
49+
lastChunk.push(expression);
50+
}
51+
}
52+
53+
return chunks.map(chunk => wrapInTable(...chunk.map(expression => context.transformExpression(expression))));
54+
}
55+
56+
export function flattenSpreadExpressions(
57+
context: TransformationContext,
58+
expressions: readonly ts.Expression[]
59+
): lua.Expression[] {
60+
const [preSpreadExpressions, postSpreadExpressions] = getExpressionsBeforeAndAfterFirstSpread(expressions);
61+
const transformedPreSpreadExpressions = preSpreadExpressions.map(a => context.transformExpression(a));
62+
63+
// Nothing special required
64+
if (postSpreadExpressions.length === 0) {
65+
return transformedPreSpreadExpressions;
66+
}
67+
68+
// Only one spread element at the end? Will work as expected
69+
if (postSpreadExpressions.length === 1) {
70+
return [...transformedPreSpreadExpressions, context.transformExpression(postSpreadExpressions[0])];
71+
}
72+
73+
// Use Array.concat and unpack the result of that as the last Expression
74+
const concatArguments = transformSpreadableExpressionsIntoArrayConcatArguments(context, postSpreadExpressions);
75+
const lastExpression = createUnpackCall(
76+
context,
77+
transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...concatArguments)
78+
);
79+
80+
return [...transformedPreSpreadExpressions, lastExpression];
81+
}
82+
1883
export function transformArguments(
1984
context: TransformationContext,
2085
params: readonly ts.Expression[],
2186
signature?: ts.Signature,
2287
callContext?: ts.Expression
2388
): lua.Expression[] {
24-
const parameters = params.map(param => context.transformExpression(param));
89+
const parameters = flattenSpreadExpressions(context, params);
2590

2691
// Add context as first param if present
2792
if (callContext) {

src/transformation/visitors/literal.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createSafeName, hasUnsafeIdentifierName, hasUnsafeSymbolName } from "..
99
import { getSymbolIdOfSymbol, trackSymbolReference } from "../utils/symbols";
1010
import { isArrayType } from "../utils/typescript";
1111
import { transformFunctionLikeDeclaration } from "./function";
12+
import { flattenSpreadExpressions } from "./call";
1213

1314
// TODO: Move to object-literal.ts?
1415
export function transformPropertyName(context: TransformationContext, node: ts.PropertyName): lua.Expression {
@@ -128,13 +129,10 @@ const transformObjectLiteralExpression: FunctionVisitor<ts.ObjectLiteralExpressi
128129
};
129130

130131
const transformArrayLiteralExpression: FunctionVisitor<ts.ArrayLiteralExpression> = (expression, context) => {
131-
const values = expression.elements.map(element =>
132-
lua.createTableFieldExpression(
133-
ts.isOmittedExpression(element) ? lua.createNilLiteral(element) : context.transformExpression(element),
134-
undefined,
135-
element
136-
)
132+
const filteredElements = expression.elements.map(e =>
133+
ts.isOmittedExpression(e) ? ts.createIdentifier("undefined") : e
137134
);
135+
const values = flattenSpreadExpressions(context, filteredElements).map(e => lua.createTableFieldExpression(e));
138136

139137
return lua.createTableExpression(values, expression);
140138
};

test/unit/spread.spec.ts

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,78 @@
11
import * as tstl from "../../src";
22
import * as util from "../util";
3+
import { formatCode } from "../util";
34

45
// TODO: Make some utils for testing other targets
56
const expectUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toMatch(/[^.]unpack\(/);
67
const expectTableUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("table.unpack");
78

9+
const arrayLiteralCases = [
10+
"1, 2, ...[3, 4, 5]",
11+
"...[1, 2], 3, 4, 5",
12+
"1, ...[[2]], 3",
13+
"...[1, 2, 3], 4, ...[5, 6]",
14+
"1, 2, ...[3, 4], ...[5, 6]",
15+
];
16+
17+
const tupleReturnDefinition = `
18+
/** @tupleReturn */
19+
function tuple(...args: any[]) {
20+
return args;
21+
}`;
22+
23+
describe.each(["function call", "array literal"] as const)("in %s", kind => {
24+
// prettier-ignore
25+
const factory = (code: string) => kind === "function call"
26+
? `((...args: any[]) => args)(${code})`
27+
: `[${code}]`;
28+
29+
test.each(arrayLiteralCases)("of array literal (%p)", expression => {
30+
util.testExpression(factory(expression)).expectToMatchJsResult();
31+
});
32+
33+
test.each(arrayLiteralCases)("of tuple return call (%p)", expression => {
34+
util.testFunction`
35+
${tupleReturnDefinition}
36+
return ${factory(`...tuple(${expression})`)};
37+
`.expectToMatchJsResult();
38+
});
39+
40+
test("of multiple string literals", () => {
41+
util.testExpression(factory('..."spread", ..."string"')).expectToMatchJsResult();
42+
});
43+
44+
test.each(["", "string", "string with spaces", "string 1 2 3"])("of string literal (%p)", str => {
45+
util.testExpression(factory(`...${formatCode(str)}`)).expectToMatchJsResult();
46+
});
47+
48+
test("of iterable", () => {
49+
util.testFunction`
50+
const it = {
51+
i: -1,
52+
[Symbol.iterator]() {
53+
return this;
54+
},
55+
next() {
56+
++this.i;
57+
return {
58+
value: 2 ** this.i,
59+
done: this.i == 9,
60+
}
61+
}
62+
};
63+
64+
return ${factory("...it")};
65+
`.expectToMatchJsResult();
66+
});
67+
});
68+
869
describe("in function call", () => {
970
util.testEachVersion(
1071
undefined,
1172
() => util.testFunction`
1273
function foo(a: number, b: number, ...rest: number[]) {
1374
return { a, b, rest }
1475
}
15-
1676
const array = [0, 1, 2, 3] as const;
1777
return foo(...array);
1878
`,
@@ -26,34 +86,17 @@ describe("in function call", () => {
2686
});
2787

2888
describe("in array literal", () => {
29-
util.testEachVersion("of array literal", () => util.testExpression`[...[0, 1, 2]]`, {
89+
util.testEachVersion(undefined, () => util.testExpression`[...[0, 1, 2]]`, {
3090
[tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack),
3191
[tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack),
3292
[tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack),
3393
[tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(),
3494
});
3595

36-
test.each(["", "string", "string with spaces", "string 1 2 3"])("of string literal (%p)", str => {
37-
util.testExpressionTemplate`[...${str}]`.expectToMatchJsResult();
38-
});
39-
40-
test("of iterable", () => {
96+
test("of array literal /w OmittedExpression", () => {
4197
util.testFunction`
42-
const it = {
43-
i: -1,
44-
[Symbol.iterator]() {
45-
return this;
46-
},
47-
next() {
48-
++this.i;
49-
return {
50-
value: 2 ** this.i,
51-
done: this.i == 9,
52-
}
53-
}
54-
};
55-
56-
return [...it]
98+
const array = [1, 2, ...[3], , 5];
99+
return { a: array[0], b: array[1], c: array[2], d: array[3] };
57100
`.expectToMatchJsResult();
58101
});
59102
});

0 commit comments

Comments
 (0)
X Tutup