Skip to content

Commit d13f800

Browse files
authored
Fixed multi returning all values (#1428)
We try to avoid using `select()` when we use `[0]` to access the first value of multiple returns (e.g. `multiCall()[0]`) In that case, we used to simply omit the `select()`. Unfortunately, this only works if the value is assigned directly (`local x = multiCall()`). In cases where the value is not assigned, omitting the select and keeping the original expression (`multiCall()`) will lead to all return values being passed on. (See #1411) To fix this, we now wrap the multireturncall in parentheses. using a new Lua parentheses expression. Using parenthesis is the correct way to keep only the first value of multiple returns (see https://www.lua.org/manual/5.1/manual.html#2.5) This fixes #1411
1 parent bfba5b7 commit d13f800

File tree

5 files changed

+70
-8
lines changed

5 files changed

+70
-8
lines changed

src/LuaAST.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export enum SyntaxKind {
4444
MethodCallExpression,
4545
Identifier,
4646
TableIndexExpression,
47+
ParenthesizedExpression,
4748

4849
// Operators
4950

@@ -843,3 +844,20 @@ export function isInlineFunctionExpression(expression: FunctionExpression): expr
843844
(expression.flags & NodeFlags.Inline) !== 0
844845
);
845846
}
847+
848+
export type ParenthesizedExpression = Expression & {
849+
expression: Expression;
850+
};
851+
852+
export function isParenthesizedExpression(node: Node): node is ParenthesizedExpression {
853+
return node.kind === SyntaxKind.ParenthesizedExpression;
854+
}
855+
856+
export function createParenthesizedExpression(expression: Expression, tsOriginal?: ts.Node): ParenthesizedExpression {
857+
const parenthesizedExpression = createNode(
858+
SyntaxKind.ParenthesizedExpression,
859+
tsOriginal
860+
) as ParenthesizedExpression;
861+
parenthesizedExpression.expression = expression;
862+
return parenthesizedExpression;
863+
}

src/LuaPrinter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,8 @@ export class LuaPrinter {
612612
return this.printIdentifier(expression as lua.Identifier);
613613
case lua.SyntaxKind.TableIndexExpression:
614614
return this.printTableIndexExpression(expression as lua.TableIndexExpression);
615+
case lua.SyntaxKind.ParenthesizedExpression:
616+
return this.printParenthesizedExpression(expression as lua.ParenthesizedExpression);
615617
default:
616618
throw new Error(`Tried to print unknown statement kind: ${lua.SyntaxKind[expression.kind]}`);
617619
}
@@ -822,6 +824,10 @@ export class LuaPrinter {
822824
return this.createSourceNode(expression, chunks);
823825
}
824826

827+
public printParenthesizedExpression(expression: lua.ParenthesizedExpression) {
828+
return this.createSourceNode(expression, ["(", this.printExpression(expression.expression), ")"]);
829+
}
830+
825831
public printOperator(kind: lua.Operator): SourceNode {
826832
return new SourceNode(null, null, this.relativeSourcePath, LuaPrinter.operatorMap[kind]);
827833
}

src/transformation/visitors/access.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ export function transformElementAccessExpressionWithCapture(
7979
context.diagnostics.push(invalidMultiReturnAccess(node));
8080
}
8181

82-
// When selecting the first element, we can shortcut
83-
if (ts.isNumericLiteral(node.argumentExpression) && node.argumentExpression.text === "0") {
84-
return { expression: table };
85-
} else {
86-
const selectIdentifier = lua.createIdentifier("select");
87-
return { expression: lua.createCallExpression(selectIdentifier, [updatedAccessExpression, table]) };
82+
const canOmitSelect = ts.isNumericLiteral(node.argumentExpression) && node.argumentExpression.text === "0";
83+
if (canOmitSelect) {
84+
// wrapping in parenthesis ensures only the first return value is used
85+
// https://www.lua.org/manual/5.1/manual.html#2.5
86+
return { expression: lua.createParenthesizedExpression(table) };
8887
}
88+
89+
const selectIdentifier = lua.createIdentifier("select");
90+
return { expression: lua.createCallExpression(selectIdentifier, [updatedAccessExpression, table]) };
8991
}
9092

9193
if (thisValueCapture) {

test/unit/language-extensions/multi.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,38 @@ test("LuaMultiReturn applies after casting a function (#1404)", () => {
338338
.withLanguageExtensions()
339339
.expectToEqual([3, 4]);
340340
});
341+
342+
// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1411
343+
describe("LuaMultiReturn returns all values even when indexed with [0] #1411", () => {
344+
const sharedCode = `
345+
declare function select<T>(this:void, index: '#', ...args: T[]): number;
346+
347+
function foo(this: void): LuaMultiReturn<[number, number]> {
348+
return $multi(123, 456);
349+
}
350+
351+
function bar(this: void): number {
352+
return foo()[0];
353+
}
354+
`;
355+
test("Returns the correct value", () => {
356+
util.testFunction`
357+
return bar();
358+
`
359+
.setTsHeader(sharedCode)
360+
.withLanguageExtensions()
361+
.expectToEqual(123);
362+
});
363+
364+
// We require an extra test since the test above would succeed even if bar() returned more than one value.
365+
// This is because our test helper always returns just the first lua value returned from the testFunction.
366+
// Here we need to test explicitly that only one value is returned.
367+
test("Does not return multiple values", () => {
368+
util.testFunction`
369+
return select("#", bar());
370+
`
371+
.setTsHeader(sharedCode)
372+
.withLanguageExtensions()
373+
.expectToEqual(1);
374+
});
375+
});

test/util.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export abstract class TestBuilder {
161161
skipLibCheck: true,
162162
target: ts.ScriptTarget.ES2017,
163163
lib: ["lib.esnext.d.ts"],
164-
moduleResolution: ts.ModuleResolutionKind.NodeJs,
164+
moduleResolution: ts.ModuleResolutionKind.Node10,
165165
resolveJsonModule: true,
166166
experimentalDecorators: true,
167167
sourceMap: true,
@@ -173,7 +173,8 @@ export abstract class TestBuilder {
173173
}
174174

175175
public withLanguageExtensions(): this {
176-
this.setOptions({ types: [path.resolve(__dirname, "..", "language-extensions")] });
176+
const langExtTypes = path.resolve(__dirname, "..", "language-extensions");
177+
this.options.types = this.options.types ? [...this.options.types, langExtTypes] : [langExtTypes];
177178
// Polyfill lualib for JS
178179
this.setJsHeader(`
179180
function $multi(...args) { return args; }

0 commit comments

Comments
 (0)