Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f8f5ccb
Use smarter expression spreading
hazzard993 Feb 27, 2020
18c1e08
Fix up test to check that concat doesn't flatten args
hazzard993 Feb 27, 2020
f37503f
flattenExpressions -> flattenSpreadExpressions
hazzard993 Feb 27, 2020
972dde8
Use for-of instead of reduce and rename flattenExpressions
hazzard993 Feb 27, 2020
352763f
Move test cases up to in function call and in array literal
hazzard993 Feb 27, 2020
478d179
Update src/transformation/visitors/call.ts
hazzard993 Feb 29, 2020
debfa31
Update src/transformation/visitors/call.ts
hazzard993 Feb 29, 2020
43400fc
Add test case to check concat
hazzard993 Mar 2, 2020
0d0af75
Add in array literal/of tuple return call test
hazzard993 Mar 2, 2020
aa2c8d2
Add test for potential edge case
hazzard993 Mar 2, 2020
c8ebe4e
Add in array literal/of array literal /w OmittedExpression
hazzard993 Mar 2, 2020
a8dee71
Refactor unneeded check in call
hazzard993 Mar 2, 2020
185751c
Allow spreadable expressions in any order
hazzard993 Mar 2, 2020
e197a68
Add of string literal mixed
hazzard993 Mar 2, 2020
4aaa38d
Update test/unit/spread.spec.ts
hazzard993 Mar 11, 2020
fd47235
Update test/unit/spread.spec.ts
hazzard993 Mar 11, 2020
5ce44ae
of string literal mixed -> of string literal
hazzard993 Mar 11, 2020
11372c4
spreadCases -> arrayLiteralCases
hazzard993 Mar 11, 2020
0381698
Update test/unit/spread.spec.ts
hazzard993 Mar 11, 2020
5c27b9a
Add tuple return tests to in function call
hazzard993 Mar 11, 2020
a62ff2a
Merge branch 'smarter-spreading' of https://github.com/hazzard993/Typ…
hazzard993 Mar 11, 2020
53de6be
Seperate spread to in function and in array literal
hazzard993 Mar 11, 2020
59b44f0
Join spread function call and array literal tests
hazzard993 Mar 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion src/transformation/visitors/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,78 @@ import { transformLuaTableCallExpression } from "./lua-table";

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

function getExpressionsBeforeAndAfterFirstSpread(
expressions: readonly ts.Expression[]
): [readonly ts.Expression[], readonly ts.Expression[]] {
// [a, b, ...c, d, ...e] --> [a, b] and [...c, d, ...e]
const index = expressions.findIndex(ts.isSpreadElement);
const hasSpreadElement = index !== -1;
const before = hasSpreadElement ? expressions.slice(0, index) : expressions;
const after = hasSpreadElement ? expressions.slice(index) : [];
return [before, after];
}

function transformSpreadableExpressionsIntoArrayConcatArguments(
context: TransformationContext,
expressions: readonly ts.Expression[] | ts.NodeArray<ts.Expression>
): lua.Expression[] {
// [...array, a, b, ...tuple()] --> [ [...array], [a, b], [...tuple()] ]
// chunk non-spread arguments together so they don't concat
const chunks: ts.Expression[][] = [];
for (const [index, expression] of expressions.entries()) {
if (ts.isSpreadElement(expression)) {
chunks.push([expression]);
const next = expressions[index + 1];
if (next && !ts.isSpreadElement(next)) {
chunks.push([]);
}
} else {
let lastChunk = chunks[chunks.length - 1];
if (!lastChunk) {
lastChunk = [];
chunks.push(lastChunk);
}
lastChunk.push(expression);
}
}

return chunks.map(chunk => wrapInTable(...chunk.map(expression => context.transformExpression(expression))));
}

export function flattenSpreadExpressions(
context: TransformationContext,
expressions: readonly ts.Expression[]
): lua.Expression[] {
const [preSpreadExpressions, postSpreadExpressions] = getExpressionsBeforeAndAfterFirstSpread(expressions);
const transformedPreSpreadExpressions = preSpreadExpressions.map(a => context.transformExpression(a));

// Nothing special required
if (postSpreadExpressions.length === 0) {
return transformedPreSpreadExpressions;
}

// Only one spread element at the end? Will work as expected
if (postSpreadExpressions.length === 1) {
return [...transformedPreSpreadExpressions, context.transformExpression(postSpreadExpressions[0])];
}

// Use Array.concat and unpack the result of that as the last Expression
const concatArguments = transformSpreadableExpressionsIntoArrayConcatArguments(context, postSpreadExpressions);
const lastExpression = createUnpackCall(
context,
transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...concatArguments)
);

return [...transformedPreSpreadExpressions, lastExpression];
}

export function transformArguments(
context: TransformationContext,
params: readonly ts.Expression[],
signature?: ts.Signature,
callContext?: ts.Expression
): lua.Expression[] {
const parameters = params.map(param => context.transformExpression(param));
const parameters = flattenSpreadExpressions(context, params);

// Add context as first param if present
if (callContext) {
Expand Down
10 changes: 4 additions & 6 deletions src/transformation/visitors/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { getSymbolIdOfSymbol, trackSymbolReference } from "../utils/symbols";
import { isArrayType } from "../utils/typescript";
import { transformFunctionLikeDeclaration } from "./function";
import { flattenSpreadExpressions } from "./call";

// TODO: Move to object-literal.ts?
export function transformPropertyName(context: TransformationContext, node: ts.PropertyName): lua.Expression {
Expand Down Expand Up @@ -137,13 +138,10 @@ const transformObjectLiteralExpression: FunctionVisitor<ts.ObjectLiteralExpressi
};

const transformArrayLiteralExpression: FunctionVisitor<ts.ArrayLiteralExpression> = (expression, context) => {
const values = expression.elements.map(element =>
lua.createTableFieldExpression(
ts.isOmittedExpression(element) ? lua.createNilLiteral(element) : context.transformExpression(element),
undefined,
element
)
const filteredElements = expression.elements.map(e =>
ts.isOmittedExpression(e) ? ts.createIdentifier("undefined") : e
);
const values = flattenSpreadExpressions(context, filteredElements).map(e => lua.createTableFieldExpression(e));

return lua.createTableExpression(values, expression);
};
Expand Down
87 changes: 65 additions & 22 deletions test/unit/spread.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,78 @@
import * as tstl from "../../src";
import * as util from "../util";
import { formatCode } from "../util";

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

const arrayLiteralCases = [
"1, 2, ...[3, 4, 5]",
"...[1, 2], 3, 4, 5",
"1, ...[[2]], 3",
"...[1, 2, 3], 4, ...[5, 6]",
"1, 2, ...[3, 4], ...[5, 6]",
];

const tupleReturnDefinition = `
/** @tupleReturn */
function tuple(...args: any[]) {
return args;
}`;

describe.each(["function call", "array literal"] as const)("in %s", kind => {
// prettier-ignore
const factory = (code: string) => kind === "function call"
? `((...args: any[]) => args)(${code})`
: `[${code}]`;

test.each(arrayLiteralCases)("of array literal (%p)", expression => {
util.testExpression(factory(expression)).expectToMatchJsResult();
});

test.each(arrayLiteralCases)("of tuple return call (%p)", expression => {
util.testFunction`
${tupleReturnDefinition}
return ${factory(`...tuple(${expression})`)};
`.expectToMatchJsResult();
});

test("of multiple string literals", () => {
util.testExpression(factory('..."spread", ..."string"')).expectToMatchJsResult();
});

test.each(["", "string", "string with spaces", "string 1 2 3"])("of string literal (%p)", str => {
util.testExpression(factory(`...${formatCode(str)}`)).expectToMatchJsResult();
});

test("of iterable", () => {
util.testFunction`
const it = {
i: -1,
[Symbol.iterator]() {
return this;
},
next() {
++this.i;
return {
value: 2 ** this.i,
done: this.i == 9,
}
}
};

return ${factory("...it")};
`.expectToMatchJsResult();
});
});

describe("in function call", () => {
util.testEachVersion(
undefined,
() => util.testFunction`
function foo(a: number, b: number, ...rest: number[]) {
return { a, b, rest }
}

const array = [0, 1, 2, 3] as const;
return foo(...array);
`,
Expand All @@ -26,34 +86,17 @@ describe("in function call", () => {
});

describe("in array literal", () => {
util.testEachVersion("of array literal", () => util.testExpression`[...[0, 1, 2]]`, {
util.testEachVersion(undefined, () => util.testExpression`[...[0, 1, 2]]`, {
[tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack),
[tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack),
[tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack),
[tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(),
});

test.each(["", "string", "string with spaces", "string 1 2 3"])("of string literal (%p)", str => {
util.testExpressionTemplate`[...${str}]`.expectToMatchJsResult();
});

test("of iterable", () => {
test("of array literal /w OmittedExpression", () => {
util.testFunction`
const it = {
i: -1,
[Symbol.iterator]() {
return this;
},
next() {
++this.i;
return {
value: 2 ** this.i,
done: this.i == 9,
}
}
};

return [...it]
const array = [1, 2, ...[3], , 5];
return { a: array[0], b: array[1], c: array[2], d: array[3] };
`.expectToMatchJsResult();
});
});
Expand Down