Skip to content

Commit 0cb8f5f

Browse files
Multi Helper (#821)
* Add tuple helper * Elide helper import declarations * tuple validation progress * Use Visitors * Fix VariableDeclaration * Implement tuple helper * Use nil if no arguments were provided to tuple * Add destructuring assignment support * Support side effects and add more validation * Add unit tests * Add test for tuple within multiple VariableDeclarations * Can be used inside for loops * Fix syntax error in test case * Remove unnecessary boilerplate from tuple helper * Use some instead of every * Use flatMap in tuple instead of map * Use Boolean instead of ternary in tuple * Import helpers from /helpers * Do not use ts-ignore in tuple test cases * Use more specific test case names for tuple * Remove unsafe cast in for-of * Remove tuple visitors * Rename to multi and MultiReturn * Revert FunctionVisitor conversion * Rename tupleResult to multiResult * Update tsconfig.json Co-Authored-By: ark120202 <ark120202@gmail.com> * Update test/unit/helpers/multi.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * Refactor for new createVirtualProgram code * Move helper transformer to transformation * Use nullish operator instead of Boolean constructor * Attempt a CommonJS implementation of multi * Add MultiReturn alias MR * Add export in front of multi function * Simplify multi js implementation * Remove MR alias * Make multi helper global * Revert multi helper import changes * Apply suggestions from code review Co-Authored-By: ark120202 <ark120202@gmail.com> * Inline and join multi conditions * Rename to $multi * Use declare for top level $multi * Add empty tuple to MultiReturn * Add declare to MultiReturn to top level declaration * Reduce number of diagnostics for multi error * Improve multi diagnostics * Apply suggestions from code review Co-Authored-By: ark120202 <ark120202@gmail.com> * Rename utils to helpers * Include helpers inside lualib's tsconfig * Fix esnext target error * Drop support for VariableDeclarations, support only ReturnStatements * Simplify multi tests a bit * Remove multi.js * Revert "Drop support for VariableDeclarations, support only ReturnStatements" This reverts commit b7d1ec0. * State that multi can only be used in return statements * Throw an error if a helper is unknown * Improve multi nomenclature * Disallow multi function use in non-return statements * Merge helpers together into index.d.ts * Prefer for loop to filter/map/foreach * Add test to return spreaded multi type from multi type function * Rename to language extensions / extensions * Remove remaining helper references * Allow multi call as ConciseBody * Remove ts-ignore * Add check first to all multi transforms * Add multi example use case Co-authored-by: ark120202 <ark120202@gmail.com>
1 parent 3546f59 commit 0cb8f5f

File tree

17 files changed

+575
-12
lines changed

17 files changed

+575
-12
lines changed

language-extensions/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare function $multi<T extends any[]>(...values: T): MultiReturn<T>;
2+
declare type MultiReturn<T extends any[]> = T & { readonly " __multiBrand": unique symbol };

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"files": [
1414
"dist/**/*.js",
1515
"dist/**/*.lua",
16-
"dist/**/*.ts"
16+
"dist/**/*.ts",
17+
"language-extensions/**/*.ts"
1718
],
1819
"main": "dist/index.js",
1920
"types": "dist/index.d.ts",

src/lualib/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"outDir": "../../dist/lualib",
44
"target": "esnext",
55
"lib": ["esnext"],
6-
"types": [],
6+
"types": ["../../language-extensions"],
77
"skipLibCheck": true,
88

99
"noUnusedLocals": true,

src/transformation/utils/diagnostics.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,32 @@ export const unsupportedVarDeclaration = createErrorDiagnosticFactory(
142142
"`var` declarations are not supported. Use `let` or `const` instead."
143143
);
144144

145+
export const invalidMultiFunctionUse = createErrorDiagnosticFactory(
146+
"The $multi function must be called in an expression that is returned."
147+
);
148+
149+
export const invalidMultiTypeToNonArrayBindingPattern = createErrorDiagnosticFactory(
150+
"Expected an array destructuring pattern."
151+
);
152+
153+
export const invalidMultiTypeToNonArrayLiteral = createErrorDiagnosticFactory("Expected an array literal.");
154+
155+
export const invalidMultiTypeToEmptyPatternOrArrayLiteral = createErrorDiagnosticFactory(
156+
"There must be one or more elements specified here."
157+
);
158+
159+
export const invalidMultiTypeArrayBindingPatternElementInitializer = createErrorDiagnosticFactory(
160+
"This array binding pattern cannot have initializers."
161+
);
162+
163+
export const invalidMultiTypeArrayLiteralElementInitializer = createErrorDiagnosticFactory(
164+
"This array literal pattern cannot have initializers."
165+
);
166+
167+
export const unsupportedMultiFunctionAssignment = createErrorDiagnosticFactory(
168+
"Omitted expressions and BindingElements are expected here."
169+
);
170+
145171
export const annotationDeprecated = createWarningDiagnosticFactory(
146172
(kind: AnnotationKind) =>
147173
`'@${kind}' is deprecated and will be removed in a future update. Please update your code before upgrading to the next release, otherwise your project will no longer compile. ` +
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as ts from "typescript";
2+
import * as path from "path";
3+
4+
export enum ExtensionKind {
5+
MultiFunction = "MultiFunction",
6+
MultiType = "MultiType",
7+
}
8+
9+
function isSourceFileFromLanguageExtensions(sourceFile: ts.SourceFile): boolean {
10+
const extensionDirectory = path.resolve(__dirname, "../../../language-extensions");
11+
const sourceFileDirectory = path.dirname(path.normalize(sourceFile.fileName));
12+
return extensionDirectory === sourceFileDirectory;
13+
}
14+
15+
export function getExtensionKind(declaration: ts.Declaration): ExtensionKind | undefined {
16+
const sourceFile = declaration.getSourceFile();
17+
if (isSourceFileFromLanguageExtensions(sourceFile)) {
18+
if (ts.isFunctionDeclaration(declaration) && declaration?.name?.text === "$multi") {
19+
return ExtensionKind.MultiFunction;
20+
}
21+
22+
if (ts.isTypeAliasDeclaration(declaration) && declaration.name.text === "MultiReturn") {
23+
return ExtensionKind.MultiType;
24+
}
25+
26+
throw new Error("Unknown extension kind");
27+
}
28+
}

src/transformation/visitors/call.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { isValidLuaIdentifier } from "../utils/safe-names";
1111
import { isArrayType, isExpressionWithEvaluationEffect, isInDestructingAssignment } from "../utils/typescript";
1212
import { transformElementAccessArgument } from "./access";
1313
import { transformLuaTableCallExpression } from "./lua-table";
14+
import { returnsMultiType } from "./language-extensions/multi";
1415

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

@@ -250,9 +251,8 @@ export const transformCallExpression: FunctionVisitor<ts.CallExpression> = (node
250251
// TODO: Currently it's also used as an array member
251252
export const transformSpreadElement: FunctionVisitor<ts.SpreadElement> = (node, context) => {
252253
const innerExpression = context.transformExpression(node.expression);
253-
if (isTupleReturnCall(context, node.expression)) {
254-
return innerExpression;
255-
}
254+
if (isTupleReturnCall(context, node.expression)) return innerExpression;
255+
if (ts.isCallExpression(node.expression) && returnsMultiType(context, node.expression)) return innerExpression;
256256

257257
if (ts.isIdentifier(node.expression) && isVarargType(context, node.expression)) {
258258
return lua.createDotsLiteral(node);

src/transformation/visitors/expression-statement.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,18 @@ import { FunctionVisitor } from "../context";
44
import { transformBinaryExpressionStatement } from "./binary-expression";
55
import { transformLuaTableExpressionStatement } from "./lua-table";
66
import { transformUnaryExpressionStatement } from "./unary-expression";
7+
import { returnsMultiType, transformMultiDestructuringAssignmentStatement } from "./language-extensions/multi";
78

89
export const transformExpressionStatement: FunctionVisitor<ts.ExpressionStatement> = (node, context) => {
10+
if (
11+
ts.isBinaryExpression(node.expression) &&
12+
node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
13+
ts.isCallExpression(node.expression.right) &&
14+
returnsMultiType(context, node.expression.right)
15+
) {
16+
return transformMultiDestructuringAssignmentStatement(context, node);
17+
}
18+
919
const luaTableResult = transformLuaTableExpressionStatement(context, node);
1020
if (luaTableResult) {
1121
return luaTableResult;

src/transformation/visitors/function.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
1616
import { peekScope, performHoisting, popScope, pushScope, Scope, ScopeType } from "../utils/scope";
1717
import { transformIdentifier } from "./identifier";
18+
import { isMultiFunction, transformMultiCallExpressionToReturnStatement } from "./language-extensions/multi";
1819
import { transformExpressionBodyToReturnStatement } from "./return";
1920
import { transformBindingPattern } from "./variable-declaration";
2021

@@ -55,6 +56,10 @@ function isRestParameterReferenced(context: TransformationContext, identifier: l
5556

5657
export function transformFunctionBodyContent(context: TransformationContext, body: ts.ConciseBody): lua.Statement[] {
5758
if (!ts.isBlock(body)) {
59+
if (ts.isCallExpression(body) && isMultiFunction(context, body)) {
60+
return [transformMultiCallExpressionToReturnStatement(context, body)];
61+
}
62+
5863
const returnStatement = transformExpressionBodyToReturnStatement(context, body);
5964
return [returnStatement];
6065
}

src/transformation/visitors/identifier.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ import * as lua from "../../LuaAST";
33
import { transformBuiltinIdentifierExpression } from "../builtins";
44
import { FunctionVisitor, TransformationContext } from "../context";
55
import { isForRangeType } from "../utils/annotations";
6-
import { invalidForRangeCall } from "../utils/diagnostics";
6+
import { invalidForRangeCall, invalidMultiFunctionUse } from "../utils/diagnostics";
77
import { createExportedIdentifier, getSymbolExportScope } from "../utils/export";
88
import { createSafeName, hasUnsafeIdentifierName } from "../utils/safe-names";
99
import { getIdentifierSymbolId } from "../utils/symbols";
1010
import { findFirstNodeAbove } from "../utils/typescript";
11+
import { isMultiFunctionNode } from "./language-extensions/multi";
1112

1213
export function transformIdentifier(context: TransformationContext, identifier: ts.Identifier): lua.Identifier {
14+
if (isMultiFunctionNode(context, identifier)) {
15+
context.diagnostics.push(invalidMultiFunctionUse(identifier));
16+
return lua.createAnonymousIdentifier(identifier);
17+
}
18+
1319
if (isForRangeType(context, identifier)) {
1420
const callExpression = findFirstNodeAbove(identifier, ts.isCallExpression);
1521
if (!callExpression || !callExpression.parent || !ts.isForOfStatement(callExpression.parent)) {
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import * as ts from "typescript";
2+
import * as lua from "../../../LuaAST";
3+
import * as extensions from "../../utils/language-extensions";
4+
import { TransformationContext } from "../../context";
5+
import { transformAssignmentLeftHandSideExpression } from "../binary-expression/assignments";
6+
import { transformIdentifier } from "../identifier";
7+
import { transformArguments } from "../call";
8+
import { getDependenciesOfSymbol, createExportedIdentifier } from "../../utils/export";
9+
import { createLocalOrExportedOrGlobalDeclaration } from "../../utils/lua-ast";
10+
import {
11+
invalidMultiTypeArrayBindingPatternElementInitializer,
12+
invalidMultiTypeArrayLiteralElementInitializer,
13+
invalidMultiTypeToEmptyPatternOrArrayLiteral,
14+
invalidMultiTypeToNonArrayBindingPattern,
15+
invalidMultiTypeToNonArrayLiteral,
16+
unsupportedMultiFunctionAssignment,
17+
invalidMultiFunctionUse,
18+
} from "../../utils/diagnostics";
19+
import { assert } from "../../../utils";
20+
21+
const isMultiFunctionDeclaration = (declaration: ts.Declaration): boolean =>
22+
extensions.getExtensionKind(declaration) === extensions.ExtensionKind.MultiFunction;
23+
24+
const isMultiTypeDeclaration = (declaration: ts.Declaration): boolean =>
25+
extensions.getExtensionKind(declaration) === extensions.ExtensionKind.MultiType;
26+
27+
export function isMultiFunction(context: TransformationContext, expression: ts.CallExpression): boolean {
28+
const type = context.checker.getTypeAtLocation(expression.expression);
29+
return type.symbol?.declarations?.some(isMultiFunctionDeclaration) ?? false;
30+
}
31+
32+
export function returnsMultiType(context: TransformationContext, node: ts.CallExpression): boolean {
33+
const signature = context.checker.getResolvedSignature(node);
34+
return signature?.getReturnType().aliasSymbol?.declarations?.some(isMultiTypeDeclaration) ?? false;
35+
}
36+
37+
export function isMultiFunctionNode(context: TransformationContext, node: ts.Node): boolean {
38+
const type = context.checker.getTypeAtLocation(node);
39+
return type.symbol?.declarations?.some(isMultiFunctionDeclaration) ?? false;
40+
}
41+
42+
export function transformMultiCallExpressionToReturnStatement(
43+
context: TransformationContext,
44+
expression: ts.Expression
45+
): lua.Statement {
46+
assert(ts.isCallExpression(expression));
47+
48+
const expressions = transformArguments(context, expression.arguments);
49+
return lua.createReturnStatement(expressions, expression);
50+
}
51+
52+
export function transformMultiReturnStatement(
53+
context: TransformationContext,
54+
statement: ts.ReturnStatement
55+
): lua.Statement {
56+
assert(statement.expression);
57+
58+
return transformMultiCallExpressionToReturnStatement(context, statement.expression);
59+
}
60+
61+
function transformMultiFunctionArguments(
62+
context: TransformationContext,
63+
expression: ts.CallExpression
64+
): lua.Expression[] | lua.Expression {
65+
if (!isMultiFunction(context, expression)) {
66+
return context.transformExpression(expression);
67+
}
68+
69+
if (expression.arguments.length === 0) {
70+
return lua.createNilLiteral(expression);
71+
}
72+
73+
return expression.arguments.map(e => context.transformExpression(e));
74+
}
75+
76+
export function transformMultiVariableDeclaration(
77+
context: TransformationContext,
78+
declaration: ts.VariableDeclaration
79+
): lua.Statement[] {
80+
assert(declaration.initializer);
81+
assert(ts.isCallExpression(declaration.initializer));
82+
83+
if (!ts.isArrayBindingPattern(declaration.name)) {
84+
context.diagnostics.push(invalidMultiTypeToNonArrayBindingPattern(declaration.name));
85+
return [];
86+
}
87+
88+
if (declaration.name.elements.length < 1) {
89+
context.diagnostics.push(invalidMultiTypeToEmptyPatternOrArrayLiteral(declaration.name));
90+
return [];
91+
}
92+
93+
if (declaration.name.elements.some(e => ts.isBindingElement(e) && e.initializer)) {
94+
context.diagnostics.push(invalidMultiTypeArrayBindingPatternElementInitializer(declaration.name));
95+
return [];
96+
}
97+
98+
if (isMultiFunction(context, declaration.initializer)) {
99+
context.diagnostics.push(invalidMultiFunctionUse(declaration.initializer));
100+
return [];
101+
}
102+
103+
const leftIdentifiers: lua.Identifier[] = [];
104+
105+
for (const element of declaration.name.elements) {
106+
if (ts.isBindingElement(element)) {
107+
if (ts.isIdentifier(element.name)) {
108+
leftIdentifiers.push(transformIdentifier(context, element.name));
109+
} else {
110+
context.diagnostics.push(unsupportedMultiFunctionAssignment(element));
111+
}
112+
} else if (ts.isOmittedExpression(element)) {
113+
leftIdentifiers.push(lua.createAnonymousIdentifier(element));
114+
}
115+
}
116+
117+
const rightExpressions = transformMultiFunctionArguments(context, declaration.initializer);
118+
return createLocalOrExportedOrGlobalDeclaration(context, leftIdentifiers, rightExpressions, declaration);
119+
}
120+
121+
export function transformMultiDestructuringAssignmentStatement(
122+
context: TransformationContext,
123+
statement: ts.ExpressionStatement
124+
): lua.Statement[] | undefined {
125+
assert(ts.isBinaryExpression(statement.expression));
126+
assert(ts.isCallExpression(statement.expression.right));
127+
128+
if (!ts.isArrayLiteralExpression(statement.expression.left)) {
129+
context.diagnostics.push(invalidMultiTypeToNonArrayLiteral(statement.expression.left));
130+
return [];
131+
}
132+
133+
if (statement.expression.left.elements.some(ts.isBinaryExpression)) {
134+
context.diagnostics.push(invalidMultiTypeArrayLiteralElementInitializer(statement.expression.left));
135+
return [];
136+
}
137+
138+
if (statement.expression.left.elements.length < 1) {
139+
context.diagnostics.push(invalidMultiTypeToEmptyPatternOrArrayLiteral(statement.expression.left));
140+
return [];
141+
}
142+
143+
if (isMultiFunction(context, statement.expression.right)) {
144+
context.diagnostics.push(invalidMultiFunctionUse(statement.expression.right));
145+
return [];
146+
}
147+
148+
const transformLeft = (expression: ts.Expression): lua.AssignmentLeftHandSideExpression =>
149+
ts.isOmittedExpression(expression)
150+
? lua.createAnonymousIdentifier(expression)
151+
: transformAssignmentLeftHandSideExpression(context, expression);
152+
153+
const leftIdentifiers = statement.expression.left.elements.map(transformLeft);
154+
155+
const rightExpressions = transformMultiFunctionArguments(context, statement.expression.right);
156+
157+
const trailingStatements = statement.expression.left.elements.flatMap(expression => {
158+
const symbol = context.checker.getSymbolAtLocation(expression);
159+
const dependentSymbols = symbol ? getDependenciesOfSymbol(context, symbol) : [];
160+
return dependentSymbols.map(symbol => {
161+
const identifierToAssign = createExportedIdentifier(context, lua.createIdentifier(symbol.name));
162+
return lua.createAssignmentStatement(identifierToAssign, transformLeft(expression));
163+
});
164+
});
165+
166+
return [lua.createAssignmentStatement(leftIdentifiers, rightExpressions, statement), ...trailingStatements];
167+
}
168+
169+
export function findMultiAssignmentViolations(
170+
context: TransformationContext,
171+
node: ts.ObjectLiteralExpression
172+
): ts.Node[] {
173+
const result: ts.Node[] = [];
174+
175+
for (const element of node.properties) {
176+
if (!ts.isShorthandPropertyAssignment(element)) continue;
177+
const valueSymbol = context.checker.getShorthandAssignmentValueSymbol(element);
178+
if (valueSymbol) {
179+
const declaration = valueSymbol.valueDeclaration;
180+
if (declaration && isMultiFunctionDeclaration(declaration)) {
181+
context.diagnostics.push(invalidMultiFunctionUse(element));
182+
result.push(element);
183+
}
184+
}
185+
}
186+
187+
return result;
188+
}

0 commit comments

Comments
 (0)