From 4c41fc940466891ec56a844e1c02a2bb47893049 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 29 Aug 2021 15:38:41 -0600 Subject: [PATCH 01/51] working on preceding statements implementation --- src/LuaAST.ts | 12 ++ src/transformation/context/context.ts | 52 ++++++- src/transformation/utils/lua-ast.ts | 22 ++- src/transformation/utils/safe-names.ts | 2 +- src/transformation/utils/transform.ts | 10 +- .../visitors/binary-expression/compound.ts | 19 +-- .../visitors/binary-expression/index.ts | 63 +++++++- src/transformation/visitors/call.ts | 140 ++++++++++-------- src/transformation/visitors/function.ts | 15 +- src/transformation/visitors/literal.ts | 71 +++++++-- src/transformation/visitors/spread.ts | 13 ++ 11 files changed, 304 insertions(+), 115 deletions(-) diff --git a/src/LuaAST.ts b/src/LuaAST.ts index 1ac86d3f5..ee5c9d329 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -566,6 +566,18 @@ export function createStringLiteral(value: string, tsOriginal?: ts.Node): String return expression; } +export function isLiteral( + node: Node +): node is NilLiteral | DotsLiteral | BooleanLiteral | NumericLiteral | StringLiteral { + return ( + isNilLiteral(node) || + isDotsLiteral(node) || + isBooleanLiteral(node) || + isNumericLiteral(node) || + isStringLiteral(node) + ); +} + export enum FunctionExpressionFlags { None = 1 << 0, Inline = 1 << 1, // Keep function body on same line diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index 4dbcf9e10..ac40f31a4 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -1,9 +1,10 @@ import * as ts from "typescript"; import { CompilerOptions, LuaTarget } from "../../CompilerOptions"; import * as lua from "../../LuaAST"; -import { castArray } from "../../utils"; +import { assert, castArray } from "../../utils"; import { unsupportedNodeKind } from "../utils/diagnostics"; import { unwrapVisitorResult } from "../utils/lua-ast"; +import { fixInvalidLuaIdentifier, isValidLuaIdentifier } from "../utils/safe-names"; import { ExpressionLikeNode, ObjectVisitor, StatementLikeNode, VisitorMap } from "./visitors"; export interface AllAccessorDeclarations { @@ -29,6 +30,7 @@ export class TransformationContext { public readonly diagnostics: ts.Diagnostic[] = []; public readonly checker: DiagnosticsProducingTypeChecker = this.program.getDiagnosticsProducingTypeChecker(); public readonly resolver: EmitResolver; + public readonly precedingStatementsStack: lua.Statement[][] = []; public readonly options: CompilerOptions = this.program.getCompilerOptions(); public readonly luaTarget = this.options.luaTarget ?? LuaTarget.Universal; @@ -46,6 +48,7 @@ export class TransformationContext { } private currentNodeVisitors: Array> = []; + private nextTempId = 0; public transformNode(node: ts.Node): lua.Node[]; /** @internal */ @@ -104,10 +107,55 @@ export class TransformationContext { } public transformStatements(node: StatementLikeNode | readonly StatementLikeNode[]): lua.Statement[] { - return castArray(node).flatMap(n => this.transformNode(n) as lua.Statement[]); + return castArray(node).flatMap(n => { + this.pushPrecedingStatements(); + const statements = this.transformNode(n) as lua.Statement[]; + statements.unshift(...this.popPrecedingStatements()); + return statements; + }); } public superTransformStatements(node: StatementLikeNode | readonly StatementLikeNode[]): lua.Statement[] { return castArray(node).flatMap(n => this.superTransformNode(n) as lua.Statement[]); } + + public pushPrecedingStatements() { + const precedingStatements: lua.Statement[] = []; + this.precedingStatementsStack.push(precedingStatements); + return precedingStatements; + } + + public popPrecedingStatements() { + const precedingStatements = this.precedingStatementsStack.pop(); + assert(precedingStatements); + return precedingStatements; + } + + public addPrecedingStatements(statements: lua.Statement[], prepend = false) { + const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; + if (prepend) { + precedingStatements.unshift(...statements); + } else { + precedingStatements.push(...statements); + } + } + + public createTempName(prefix = "") { + return `____${prefix}${this.nextTempId++}`; + } + + public createTempNameFromExpression(expression: lua.Expression) { + let name: string | undefined; + if (lua.isStringLiteral(expression)) { + name = expression.value; + } else if (lua.isIdentifier(expression)) { + name = expression.text; + } + if (!name) { + name = "temp"; + } else if (!isValidLuaIdentifier(name)) { + name = fixInvalidLuaIdentifier(name); + } + return `____${name}${this.nextTempId++}`; + } } diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index 5f3a21e4a..ae3d03d33 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -65,14 +65,21 @@ export function getNumberLiteralValue(expression?: lua.Expression) { // Prefer use of transformToImmediatelyInvokedFunctionExpression to maintain correct scope. If you use this directly, // ensure you push/pop a function scope appropriately to avoid incorrect vararg optimization. export function createImmediatelyInvokedFunctionExpression( + scope: Scope, statements: lua.Statement[], result: lua.Expression | lua.Expression[], tsOriginal?: ts.Node -): lua.CallExpression { - const body = [...statements, lua.createReturnStatement(castArray(result))]; - const flags = statements.length === 0 ? lua.FunctionExpressionFlags.Inline : lua.FunctionExpressionFlags.None; - const iife = lua.createFunctionExpression(lua.createBlock(body), undefined, undefined, flags); - return lua.createCallExpression(iife, [], tsOriginal); +): [lua.Statement[], lua.Expression] { + const resultName = `____result${scope.id}`; + const resultIdentifier = lua.createIdentifier(resultName, tsOriginal); + const body = [...statements, lua.createAssignmentStatement(resultIdentifier, result, tsOriginal)]; + return [ + [ + lua.createVariableDeclarationStatement(lua.cloneIdentifier(resultIdentifier), undefined, tsOriginal), + lua.createDoStatement(body, tsOriginal), + ], + lua.cloneIdentifier(resultIdentifier), + ]; } export function createUnpackCall( @@ -175,9 +182,12 @@ export function createLocalOrExportedOrGlobalDeclaration( const isTopLevelVariable = scope.type === ScopeType.File; if (context.isModule || !isTopLevelVariable) { + let precededDeclaration = false; if (scope.type === ScopeType.Switch || (!isFunctionDeclaration && hasMultipleReferences(scope, lhs))) { // Split declaration and assignment of identifiers that reference themselves in their declaration declaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal); + context.addPrecedingStatements([declaration], true); + precededDeclaration = true; if (rhs) { assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); } @@ -192,7 +202,7 @@ export function createLocalOrExportedOrGlobalDeclaration( scope.variableDeclarations.push(declaration); - if (scope.type === ScopeType.Switch) { + if (scope.type === ScopeType.Switch || precededDeclaration) { declaration = undefined; } } else if (rhs) { diff --git a/src/transformation/utils/safe-names.ts b/src/transformation/utils/safe-names.ts index 877196ba4..b458930fc 100644 --- a/src/transformation/utils/safe-names.ts +++ b/src/transformation/utils/safe-names.ts @@ -98,7 +98,7 @@ export function hasUnsafeIdentifierName( return checkName(context, identifier.text, identifier); } -const fixInvalidLuaIdentifier = (name: string) => +export const fixInvalidLuaIdentifier = (name: string) => name.replace(/[^a-zA-Z0-9_]/g, c => `_${c.charCodeAt(0).toString(16).toUpperCase()}`); export const createSafeName = (name: string) => "____" + fixInvalidLuaIdentifier(name); diff --git a/src/transformation/utils/transform.ts b/src/transformation/utils/transform.ts index 215a018d5..fa1280ae7 100644 --- a/src/transformation/utils/transform.ts +++ b/src/transformation/utils/transform.ts @@ -14,9 +14,11 @@ export function transformToImmediatelyInvokedFunctionExpression( context: TransformationContext, transformFunction: () => ImmediatelyInvokedFunctionParameters, tsOriginal?: ts.Node -): lua.CallExpression { - pushScope(context, ScopeType.Function); - const { statements, result } = transformFunction(); +): lua.Expression { + const scope = pushScope(context, ScopeType.Block); + let { statements, result } = transformFunction(); + [statements, result] = createImmediatelyInvokedFunctionExpression(scope, castArray(statements), result, tsOriginal); + context.addPrecedingStatements(statements); popScope(context); - return createImmediatelyInvokedFunctionExpression(castArray(statements), result, tsOriginal); + return result; } diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index febd7681e..0650a1856 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -142,7 +142,7 @@ export function transformCompoundAssignment( if (isSetterSkippingCompoundAssignmentOperator(operator)) { const statements = [ tmpDeclaration, - ...transformSetterSkippingCompoundAssignment(context, tmpIdentifier, operator, rhs), + ...transformSetterSkippingCompoundAssignment(tmpIdentifier, operator, right), ]; return { statements, result: tmpIdentifier }; } @@ -165,7 +165,7 @@ export function transformCompoundAssignmentExpression( rhs: ts.Expression, operator: CompoundAssignmentToken, isPostfix: boolean -): lua.CallExpression { +): lua.Expression { return transformToImmediatelyInvokedFunctionExpression( context, () => transformCompoundAssignment(context, expression, lhs, rhs, operator, isPostfix), @@ -199,7 +199,7 @@ export function transformCompoundAssignmentStatement( if (isSetterSkippingCompoundAssignmentOperator(operator)) { return [ objAndIndexDeclaration, - ...transformSetterSkippingCompoundAssignment(context, accessExpression, operator, rhs, node), + ...transformSetterSkippingCompoundAssignment(accessExpression, operator, right, node), ]; } @@ -208,8 +208,7 @@ export function transformCompoundAssignmentStatement( return [objAndIndexDeclaration, assignStatement]; } else { if (isSetterSkippingCompoundAssignmentOperator(operator)) { - const luaLhs = context.transformExpression(lhs) as lua.AssignmentLeftHandSideExpression; - return transformSetterSkippingCompoundAssignment(context, luaLhs, operator, rhs, node); + return transformSetterSkippingCompoundAssignment(left, operator, right, node); } // Simple statements @@ -237,10 +236,9 @@ function isSetterSkippingCompoundAssignmentOperator( } function transformSetterSkippingCompoundAssignment( - context: TransformationContext, lhs: lua.AssignmentLeftHandSideExpression, operator: SetterSkippingCompoundAssignmentOperator, - rhs: ts.Expression, + right: lua.Expression, node?: ts.Node ): lua.Statement[] { // These assignments have the form 'if x then y = z', figure out what condition x is first. @@ -258,11 +256,6 @@ function transformSetterSkippingCompoundAssignment( // if condition then lhs = rhs end return [ - lua.createIfStatement( - condition, - lua.createBlock([lua.createAssignmentStatement(lhs, context.transformExpression(rhs))]), - undefined, - node - ), + lua.createIfStatement(condition, lua.createBlock([lua.createAssignmentStatement(lhs, right)]), undefined, node), ]; } diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 93ea66018..055476b80 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -15,6 +15,7 @@ import { } from "./compound"; import { assert } from "../../../utils"; import { transformToImmediatelyInvokedFunctionExpression } from "../../utils/transform"; +// import { peekScope } from "../../utils/scope"; type SimpleOperator = | ts.AdditiveOperatorOrHigher @@ -76,6 +77,27 @@ export function transformBinaryOperation( return lua.createBinaryExpression(left, right, luaOperator, node); } +function createShortCircuitBinaryExpression( + context: TransformationContext, + node: ts.BinaryExpression, + createCondition: (identifier: lua.Identifier) => lua.Expression +) { + const lhs = context.transformExpression(node.left); + context.pushPrecedingStatements(); + const rhs = context.transformExpression(node.right); + const rightPrecedingStatements = context.popPrecedingStatements(); + if (rightPrecedingStatements.length > 0) { + const result = lua.createIdentifier(context.createTempNameFromExpression(lhs)); + const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs); + const ifStatement = lua.createIfStatement( + createCondition(lua.cloneIdentifier(result)), + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(result, rhs)]) + ); + context.addPrecedingStatements([assignmentStatement, ifStatement]); + return result; + } +} + export const transformBinaryExpression: FunctionVisitor = (node, context) => { const operator = node.operatorToken.kind; @@ -128,15 +150,42 @@ export const transformBinaryExpression: FunctionVisitor = ( ); } - default: - return transformBinaryOperation( - context, - context.transformExpression(node.left), - context.transformExpression(node.right), - operator, - node + case ts.SyntaxKind.QuestionQuestionToken: { + const expression = createShortCircuitBinaryExpression(context, node, i => + lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.InequalityOperator) ); + if (expression) { + return expression; + } + break; + } + + case ts.SyntaxKind.BarBarToken: { + const expression = createShortCircuitBinaryExpression(context, node, i => + lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator) + ); + if (expression) { + return expression; + } + break; + } + + case ts.SyntaxKind.AmpersandAmpersandToken: { + const expression = createShortCircuitBinaryExpression(context, node, i => i); + if (expression) { + return expression; + } + break; + } } + + return transformBinaryOperation( + context, + context.transformExpression(node.left), + context.transformExpression(node.right), + operator, + node + ); }; export function transformBinaryExpressionStatement( diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 22adb9a45..38f6abc59 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -10,7 +10,10 @@ import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { isValidLuaIdentifier } from "../utils/safe-names"; import { isExpressionWithEvaluationEffect } from "../utils/typescript"; import { transformElementAccessArgument } from "./access"; -import { isMultiReturnCall, shouldMultiReturnCallBeWrapped } from "./language-extensions/multi"; +import { + isMultiReturnCall, + /* isMultiReturnType, */ shouldMultiReturnCallBeWrapped, +} from "./language-extensions/multi"; import { isOperatorMapping, transformOperatorMappingExpression } from "./language-extensions/operators"; import { isTableDeleteCall, @@ -23,76 +26,77 @@ import { transformTableSetExpression, } from "./language-extensions/table"; import { annotationRemoved, invalidTableDeleteExpression, invalidTableSetExpression } from "../utils/diagnostics"; -import { - ImmediatelyInvokedFunctionParameters, - transformToImmediatelyInvokedFunctionExpression, -} from "../utils/transform"; +import { transformToImmediatelyInvokedFunctionExpression } from "../utils/transform"; +import { isOptimizedVarArgSpreadElement } from "./spread"; 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 -): 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)); + const transformedExpressions: lua.Expression[] = []; + const unwrapInConcat: boolean[] = []; + let lastExpressionWithPrecedingStatements = -1; + for (let i = 0; i < expressions.length; ++i) { + context.pushPrecedingStatements(); + const transformedExpression = context.transformExpression(expressions[i]); + const precedingStatements = context.popPrecedingStatements(); + + // If preceding statements were generated, walk back and cache previous values in temps + if (precedingStatements.length > 0) { + for (let j = lastExpressionWithPrecedingStatements + 1; j < i; ++j) { + let previousExpression = transformedExpressions[j]; + if (!lua.isLiteral(previousExpression)) { + const tempVar = lua.createIdentifier(context.createTempNameFromExpression(previousExpression)); + if (ts.isSpreadElement(expressions[j])) { + previousExpression = wrapInTable(previousExpression); + unwrapInConcat[j] = true; + } + context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(tempVar, previousExpression), + ]); + transformedExpressions[j] = lua.cloneIdentifier(tempVar); + } + } + lastExpressionWithPrecedingStatements = i; - // Nothing special required - if (postSpreadExpressions.length === 0) { - return transformedPreSpreadExpressions; - } + // Bubble up preceding statements + context.addPrecedingStatements(precedingStatements); + } - // Only one spread element at the end? Will work as expected - if (postSpreadExpressions.length === 1) { - return [...transformedPreSpreadExpressions, context.transformExpression(postSpreadExpressions[0])]; + transformedExpressions.push(transformedExpression); + unwrapInConcat.push(false); } - // 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) + // If there are spreads in the middle, use the array concat lib function + const firstSpreadIndex = expressions.findIndex( + e => ts.isSpreadElement(e) && !isOptimizedVarArgSpreadElement(context, e) ); + if (firstSpreadIndex >= 0 && firstSpreadIndex < expressions.length - 1) { + const tbls: lua.Expression[] = []; + let tbl: lua.Expression[] = []; + for (let i = 0; i < expressions.length; ++i) { + let transformedExpression = transformedExpressions[i]; + if (ts.isSpreadElement(expressions[i])) { + if (unwrapInConcat[i]) { + transformedExpression = createUnpackCall(context, transformedExpression); + } + tbls.push(wrapInTable(...tbl, transformedExpression)); + tbl = []; + } else { + tbl.push(transformedExpression); + } + } + if (tbl.length > 0) { + tbls.push(wrapInTable(...tbl)); + } + return [ + createUnpackCall(context, transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...tbls)), + ]; + } - return [...transformedPreSpreadExpressions, lastExpression]; + return transformedExpressions; } export function transformArguments( @@ -127,15 +131,20 @@ function transformElementAccessCall( left: ts.PropertyAccessExpression | ts.ElementAccessExpression, args: ts.Expression[] | ts.NodeArray, signature?: ts.Signature -): ImmediatelyInvokedFunctionParameters { - const transformedArguments = transformArguments(context, args, signature, ts.factory.createIdentifier("____self")); +): { statements: lua.Statement; result: lua.CallExpression } { + const selfIdentifier = lua.createIdentifier(context.createTempName("self")); + const transformedArguments = transformArguments( + context, + args, + signature, + ts.factory.createIdentifier(selfIdentifier.text) + ); // Cache left-side if it has effects // (function() local ____self = context; return ____self[argument](parameters); end)() const argument = ts.isElementAccessExpression(left) ? transformElementAccessArgument(context, left) : lua.createStringLiteral(left.name.text); - const selfIdentifier = lua.createIdentifier("____self"); const callContext = context.transformExpression(left.expression); const selfAssignment = lua.createVariableDeclarationStatement(selfIdentifier, callContext); const index = lua.createTableIndexExpression(selfIdentifier, argument); @@ -174,11 +183,14 @@ export function transformContextualCallExpression( } } else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) { if (isExpressionWithEvaluationEffect(left.expression)) { - return transformToImmediatelyInvokedFunctionExpression( + const { statements: selfAssignment, result: callExpression } = transformElementAccessCall( context, - () => transformElementAccessCall(context, left, args, signature), - node + left, + args, + signature ); + context.addPrecedingStatements([selfAssignment]); + return callExpression; } else { const callContext = context.transformExpression(left.expression); const expression = context.transformExpression(left); diff --git a/src/transformation/visitors/function.ts b/src/transformation/visitors/function.ts index 166a1b87a..c65a9210d 100644 --- a/src/transformation/visitors/function.ts +++ b/src/transformation/visitors/function.ts @@ -53,8 +53,10 @@ function isRestParameterReferenced(identifier: lua.Identifier, scope: Scope): bo export function transformFunctionBodyContent(context: TransformationContext, body: ts.ConciseBody): lua.Statement[] { if (!ts.isBlock(body)) { + context.pushPrecedingStatements(); const returnStatement = transformExpressionBodyToReturnStatement(context, body); - return [returnStatement]; + const precedingStatements = context.popPrecedingStatements(); + return [...precedingStatements, returnStatement]; } const bodyStatements = performHoisting(context, context.transformStatements(body.statements)); @@ -249,10 +251,19 @@ export function transformFunctionLikeDeclaration( // the function first to determine if it's self-referencing. Fortunately, this does not cause issues // with var-arg optimization because the IIFE is just wrapping another function which will already push // another scope. - return createImmediatelyInvokedFunctionExpression( + const scope = pushScope(context, ScopeType.Block); + let statements: lua.Statement[] = [ + lua.createVariableDeclarationStatement(nameIdentifier, functionExpression), + ]; + let result: lua.Expression = lua.cloneIdentifier(nameIdentifier); + [statements, result] = createImmediatelyInvokedFunctionExpression( + scope, [lua.createVariableDeclarationStatement(nameIdentifier, functionExpression)], lua.cloneIdentifier(nameIdentifier) ); + popScope(context); + context.addPrecedingStatements(statements); + return result; } } } diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 75b4a3224..316d34613 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -71,15 +71,18 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor __TS__ObjectAssign({x = 0}, {y = 2}, {y = 1, z = 2}) - if (properties.length > 0) { - const tableExpression = lua.createTableExpression(properties, expression); - tableExpressions.push(tableExpression); - properties = []; - } - const type = context.checker.getTypeAtLocation(element.expression); let tableExpression: lua.Expression; if (isArrayType(context, type)) { @@ -129,12 +124,56 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor 0) { + tableExpressions.push(lua.createTableExpression(properties)); + } + tableExpressions.push(property); + properties = []; + } } if (tableExpressions.length === 0) { diff --git a/src/transformation/visitors/spread.ts b/src/transformation/visitors/spread.ts index f8ebdc79d..9132c1663 100644 --- a/src/transformation/visitors/spread.ts +++ b/src/transformation/visitors/spread.ts @@ -61,6 +61,19 @@ export function isOptimizedVarArgSpread(context: TransformationContext, symbol: return true; } +export function isOptimizedVarArgSpreadElement(context: TransformationContext, spreadElement: ts.SpreadElement) { + if (!ts.isIdentifier(spreadElement.expression)) { + return false; + } + + const symbol = context.checker.getSymbolAtLocation(spreadElement.expression); + if (!symbol || !isOptimizedVarArgSpread(context, symbol, spreadElement.expression)) { + return false; + } + + return true; +} + // TODO: Currently it's also used as an array member export const transformSpreadElement: FunctionVisitor = (node, context) => { if (ts.isIdentifier(node.expression)) { From 69390bf02643c09db50a84a6fd4221485d34d8dc Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 30 Aug 2021 07:02:06 -0600 Subject: [PATCH 02/51] fixed issues from tests and started adding new tests (which don't pass yet) --- src/transformation/utils/lua-ast.ts | 6 ++-- .../visitors/binary-expression/compound.ts | 32 ++++++++++++++++--- .../visitors/binary-expression/index.ts | 2 +- test/unit/precedingStatements.spec.ts | 29 +++++++++++++++++ 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 test/unit/precedingStatements.spec.ts diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index ae3d03d33..2e87f4c0d 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -186,8 +186,10 @@ export function createLocalOrExportedOrGlobalDeclaration( if (scope.type === ScopeType.Switch || (!isFunctionDeclaration && hasMultipleReferences(scope, lhs))) { // Split declaration and assignment of identifiers that reference themselves in their declaration declaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal); - context.addPrecedingStatements([declaration], true); - precededDeclaration = true; + if (scope.type !== ScopeType.Switch) { + context.addPrecedingStatements([declaration], true); + precededDeclaration = true; + } if (rhs) { assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); } diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index 0650a1856..cd272e9d2 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -87,7 +87,9 @@ export function transformCompoundAssignment( isPostfix: boolean ): ImmediatelyInvokedFunctionParameters { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); + context.pushPrecedingStatements(); const right = context.transformExpression(rhs); + const rightPrecedingStatements = context.popPrecedingStatements(); const [objExpression, indexExpression] = parseAccessExpressionWithEvaluationEffects(context, lhs); if (objExpression && indexExpression) { @@ -118,6 +120,7 @@ export function transformCompoundAssignment( assignStatement = lua.createAssignmentStatement(accessExpression, tmp); } // return ____tmp + context.addPrecedingStatements(rightPrecedingStatements); return { statements: [objAndIndexDeclaration, tmpDeclaration, assignStatement], result: tmp }; } else if (isPostfix) { // Postfix expressions need to cache original value in temp @@ -128,6 +131,7 @@ export function transformCompoundAssignment( const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); const operatorExpression = transformBinaryOperation(context, tmpIdentifier, right, operator, expression); const assignStatements = transformAssignment(context, lhs, operatorExpression); + context.addPrecedingStatements(rightPrecedingStatements); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else if (ts.isPropertyAccessExpression(lhs) || ts.isElementAccessExpression(lhs)) { // Simple property/element access expressions need to cache in temp to avoid double-evaluation @@ -142,17 +146,19 @@ export function transformCompoundAssignment( if (isSetterSkippingCompoundAssignmentOperator(operator)) { const statements = [ tmpDeclaration, - ...transformSetterSkippingCompoundAssignment(tmpIdentifier, operator, right), + ...transformSetterSkippingCompoundAssignment(tmpIdentifier, operator, right, rightPrecedingStatements), ]; return { statements, result: tmpIdentifier }; } + context.addPrecedingStatements(rightPrecedingStatements); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else { // Simple expressions // ${left} = ${right}; return ${right} const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); const statements = transformAssignment(context, lhs, operatorExpression); + context.addPrecedingStatements(rightPrecedingStatements); return { statements, result: left }; } } @@ -181,7 +187,9 @@ export function transformCompoundAssignmentStatement( operator: CompoundAssignmentToken ): lua.Statement[] { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); + context.pushPrecedingStatements(); const right = context.transformExpression(rhs); + const rightPrecedingStatements = context.popPrecedingStatements(); const [objExpression, indexExpression] = parseAccessExpressionWithEvaluationEffects(context, lhs); if (objExpression && indexExpression) { @@ -199,21 +207,29 @@ export function transformCompoundAssignmentStatement( if (isSetterSkippingCompoundAssignmentOperator(operator)) { return [ objAndIndexDeclaration, - ...transformSetterSkippingCompoundAssignment(accessExpression, operator, right, node), + ...transformSetterSkippingCompoundAssignment( + accessExpression, + operator, + right, + rightPrecedingStatements, + node + ), ]; } const operatorExpression = transformBinaryOperation(context, accessExpression, right, operator, node); const assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); + context.addPrecedingStatements(rightPrecedingStatements); return [objAndIndexDeclaration, assignStatement]; } else { if (isSetterSkippingCompoundAssignmentOperator(operator)) { - return transformSetterSkippingCompoundAssignment(left, operator, right, node); + return transformSetterSkippingCompoundAssignment(left, operator, right, rightPrecedingStatements, node); } // Simple statements // ${left} = ${left} ${replacementOperator} ${right} const operatorExpression = transformBinaryOperation(context, left, right, operator, node); + context.addPrecedingStatements(rightPrecedingStatements); return transformAssignment(context, lhs, operatorExpression); } } @@ -239,6 +255,7 @@ function transformSetterSkippingCompoundAssignment( lhs: lua.AssignmentLeftHandSideExpression, operator: SetterSkippingCompoundAssignmentOperator, right: lua.Expression, + rightPrecedingStatements: lua.Statement[], node?: ts.Node ): lua.Statement[] { // These assignments have the form 'if x then y = z', figure out what condition x is first. @@ -248,7 +265,7 @@ function transformSetterSkippingCompoundAssignment( condition = lhs; } else if (operator === ts.SyntaxKind.BarBarToken) { condition = lua.createUnaryExpression(lhs, lua.SyntaxKind.NotOperator); - } else if (operator === ts.SyntaxKind.QuestionQuestionToken) { + } else if (isSetterSkippingCompoundAssignmentOperator(operator)) { condition = lua.createBinaryExpression(lhs, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator); } else { assertNever(operator); @@ -256,6 +273,11 @@ function transformSetterSkippingCompoundAssignment( // if condition then lhs = rhs end return [ - lua.createIfStatement(condition, lua.createBlock([lua.createAssignmentStatement(lhs, right)]), undefined, node), + lua.createIfStatement( + condition, + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(lhs, right)]), + undefined, + node + ), ]; } diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 055476b80..640eea219 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -152,7 +152,7 @@ export const transformBinaryExpression: FunctionVisitor = ( case ts.SyntaxKind.QuestionQuestionToken: { const expression = createShortCircuitBinaryExpression(context, node, i => - lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.InequalityOperator) + lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator) ); if (expression) { return expression; diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts new file mode 100644 index 000000000..8743fbe99 --- /dev/null +++ b/test/unit/precedingStatements.spec.ts @@ -0,0 +1,29 @@ +import * as util from "../util"; + +test.each([ + { x: 1, op: "&&" }, + { x: false, op: "&&" }, + { x: null, op: "&&" }, + { x: 1, op: "&&=" }, + { x: false, op: "&&=" }, + { x: null, op: "&&=" }, + { x: 1, op: "||" }, + { x: false, op: "||" }, + { x: null, op: "||" }, + { x: 1, op: "||=" }, + { x: false, op: "||=" }, + { x: null, op: "||=" }, + { x: 1, op: "??" }, + { x: false, op: "??" }, + { x: null, op: "??" }, + { x: 1, op: "??=" }, + { x: false, op: "??=" }, + { x: null, op: "??=" }, +])("short circuit operator (%p)", input => { + util.testFunction` + let x: unknown = ${input.x}; + let y = 1; + const z = x ${input.op} y++; + return {x, y, z}; + `.expectToMatchJsResult(); +}); From c30fa75d6d937e042b63463ece87b14b2a881453 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 31 Aug 2021 06:56:28 -0600 Subject: [PATCH 03/51] fixed issues with short-circuit compound operator expressions --- src/transformation/visitors/binary-expression/compound.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index cd272e9d2..eb01502ac 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -158,6 +158,14 @@ export function transformCompoundAssignment( // ${left} = ${right}; return ${right} const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); const statements = transformAssignment(context, lhs, operatorExpression); + + if (rightPrecedingStatements.length > 0 && isSetterSkippingCompoundAssignmentOperator(operator)) { + return { + statements: transformSetterSkippingCompoundAssignment(left, operator, right, rightPrecedingStatements), + result: left, + }; + } + context.addPrecedingStatements(rightPrecedingStatements); return { statements, result: left }; } From af010aa422ce3d5ec298bd822ee7d53877c2efb8 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 31 Aug 2021 06:56:41 -0600 Subject: [PATCH 04/51] execution order tests --- test/unit/precedingStatements.spec.ts | 99 +++++++++++++++++++++------ 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 8743fbe99..c3765017e 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -1,29 +1,86 @@ import * as util from "../util"; test.each([ - { x: 1, op: "&&" }, - { x: false, op: "&&" }, - { x: null, op: "&&" }, - { x: 1, op: "&&=" }, - { x: false, op: "&&=" }, - { x: null, op: "&&=" }, - { x: 1, op: "||" }, - { x: false, op: "||" }, - { x: null, op: "||" }, - { x: 1, op: "||=" }, - { x: false, op: "||=" }, - { x: null, op: "||=" }, - { x: 1, op: "??" }, - { x: false, op: "??" }, - { x: null, op: "??" }, - { x: 1, op: "??=" }, - { x: false, op: "??=" }, - { x: null, op: "??=" }, -])("short circuit operator (%p)", input => { + { operator: "&&", testValue: true }, + { operator: "&&", testValue: false }, + { operator: "&&", testValue: null }, + { operator: "&&=", testValue: true }, + { operator: "&&=", testValue: false }, + { operator: "&&=", testValue: null }, + { operator: "||", testValue: true }, + { operator: "||", testValue: false }, + { operator: "||", testValue: null }, + { operator: "||=", testValue: true }, + { operator: "||=", testValue: false }, + { operator: "||=", testValue: null }, + { operator: "??", testValue: true }, + { operator: "??", testValue: false }, + { operator: "??", testValue: null }, + { operator: "??=", testValue: true }, + { operator: "??=", testValue: false }, + { operator: "??=", testValue: null }, +])("short circuit operator (%p)", ({ operator, testValue }) => { util.testFunction` - let x: unknown = ${input.x}; + let x: unknown = ${testValue}; let y = 1; - const z = x ${input.op} y++; + const z = x ${operator} y++; return {x, y, z}; `.expectToMatchJsResult(); }); + +describe("execution order", () => { + const sequenceTests = [ + "i++, i", + "i, i++, i, i++", + "...a", + "i, ...a", + "...a, i", + "i, ...a, i++, i, ...a", + "i, ...a, i++, i, ...a, i", + "...[1, i++, 2]", + "...[1, i++, 2], i++", + "i, ...[1, i++, 2]", + "i, ...[1, i++, 2], i", + "i, ...[1, i++, 2], i++", + "i, ...[1, i++, 2], i++, ...[3, i++, 4]", + "i, ...a, i++, ...[1, i++, 2], i, i++, ...a", + ]; + + test.each(sequenceTests)("array literal ([%p])", sequence => { + util.testFunction` + const a = [7, 8, 9]; + let i = 0; + return [${sequence}]; + `.expectToMatchJsResult(); + }); + + test.each(sequenceTests)("function arguments (foo(%p))", sequence => { + util.testFunction` + const a = [7, 8, 9]; + let i = 0; + function foo(...args: unknown[]) { return args; } + return foo(${sequence}); + `.expectToMatchJsResult(); + }); + + test.each([ + "{a: i, b: i++}", + "{a: i, b: i++, c: i}", + "{a: i, ...{b: i++}, c: i}", + "{a: i, ...o, b: i++}", + "{a: i, ...[i], b: i++}", + "{a: i, ...[i++], b: i++}", + "{a: i, ...o, b: i++, ...[i], ...{c: i++}, d: i++}", + ])("object literal (%p)", literal => { + util.testFunction` + const o = {a: "A", b: "B", c: "C"}; + let i = 0; + const literal = ${literal}; + const result: Record = {}; + (Object.keys(result) as Array).forEach( + key => { result[key.toString()] = literal[key]; } + ); + return result; + `.expectToMatchJsResult(); + }); +}); From 8888089baac365d9fcf177c909c4fb7f97175191 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 1 Sep 2021 07:28:29 -0600 Subject: [PATCH 05/51] fixes for remaining broken tests --- .../visitors/binary-expression/index.ts | 37 ++++------- src/transformation/visitors/conditional.ts | 35 +++++----- src/transformation/visitors/loops/do-while.ts | 42 ++++++++---- src/transformation/visitors/loops/for.ts | 25 +++++-- src/transformation/visitors/loops/utils.ts | 8 +++ test/unit/precedingStatements.spec.ts | 65 +++++++++++++++++++ 6 files changed, 149 insertions(+), 63 deletions(-) diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 640eea219..3fc026d79 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -14,8 +14,6 @@ import { unwrapCompoundAssignmentToken, } from "./compound"; import { assert } from "../../../utils"; -import { transformToImmediatelyInvokedFunctionExpression } from "../../utils/transform"; -// import { peekScope } from "../../utils/scope"; type SimpleOperator = | ts.AdditiveOperatorOrHigher @@ -80,6 +78,7 @@ export function transformBinaryOperation( function createShortCircuitBinaryExpression( context: TransformationContext, node: ts.BinaryExpression, + operator: BitOperator | SimpleOperator | ts.SyntaxKind.QuestionQuestionToken, createCondition: (identifier: lua.Identifier) => lua.Expression ) { const lhs = context.transformExpression(node.left); @@ -95,6 +94,8 @@ function createShortCircuitBinaryExpression( ); context.addPrecedingStatements([assignmentStatement, ifStatement]); return result; + } else { + return transformBinaryOperation(context, lhs, rhs, operator, node); } } @@ -140,42 +141,28 @@ export const transformBinaryExpression: FunctionVisitor = ( } case ts.SyntaxKind.CommaToken: { - return transformToImmediatelyInvokedFunctionExpression( - context, - () => ({ - statements: context.transformStatements(ts.factory.createExpressionStatement(node.left)), - result: context.transformExpression(node.right), - }), - node - ); + const statements = context.transformStatements(ts.factory.createExpressionStatement(node.left)); + context.pushPrecedingStatements(); + const result = context.transformExpression(node.right); + statements.push(...context.popPrecedingStatements()); + context.addPrecedingStatements(statements); + return result; } case ts.SyntaxKind.QuestionQuestionToken: { - const expression = createShortCircuitBinaryExpression(context, node, i => + return createShortCircuitBinaryExpression(context, node, operator, i => lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator) ); - if (expression) { - return expression; - } - break; } case ts.SyntaxKind.BarBarToken: { - const expression = createShortCircuitBinaryExpression(context, node, i => + return createShortCircuitBinaryExpression(context, node, operator, i => lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator) ); - if (expression) { - return expression; - } - break; } case ts.SyntaxKind.AmpersandAmpersandToken: { - const expression = createShortCircuitBinaryExpression(context, node, i => i); - if (expression) { - return expression; - } - break; + return createShortCircuitBinaryExpression(context, node, operator, i => i); } } diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts index 2fab7494c..f44e9acfc 100644 --- a/src/transformation/visitors/conditional.ts +++ b/src/transformation/visitors/conditional.ts @@ -27,32 +27,29 @@ function canBeFalsy(context: TransformationContext, type: ts.Type): boolean { } } -function wrapInFunctionCall(expression: lua.Expression): lua.FunctionExpression { - const returnStatement = lua.createReturnStatement([expression]); - - return lua.createFunctionExpression( - lua.createBlock([returnStatement]), - undefined, - undefined, - lua.FunctionExpressionFlags.Inline - ); -} - function transformProtectedConditionalExpression( context: TransformationContext, expression: ts.ConditionalExpression -): lua.CallExpression { +): lua.Expression { + const tempVar = lua.createIdentifier(context.createTempName("temp")); + const condition = context.transformExpression(expression.condition); + + context.pushPrecedingStatements(); const val1 = context.transformExpression(expression.whenTrue); - const val2 = context.transformExpression(expression.whenFalse); + const trueStatements = context.popPrecedingStatements(); + trueStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val1)); - const val1Function = wrapInFunctionCall(val1); - const val2Function = wrapInFunctionCall(val2); + context.pushPrecedingStatements(); + const val2 = context.transformExpression(expression.whenFalse); + const falseStatements = context.popPrecedingStatements(); + falseStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val2)); - // (condition and (() => v1) or (() => v2))() - const conditionAnd = lua.createBinaryExpression(condition, val1Function, lua.SyntaxKind.AndOperator); - const orExpression = lua.createBinaryExpression(conditionAnd, val2Function, lua.SyntaxKind.OrOperator); - return lua.createCallExpression(orExpression, [], expression); + context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar)]); + context.addPrecedingStatements([ + lua.createIfStatement(condition, lua.createBlock(trueStatements), lua.createBlock(falseStatements), expression), + ]); + return lua.cloneIdentifier(tempVar); } export const transformConditionalExpression: FunctionVisitor = (expression, context) => { diff --git a/src/transformation/visitors/loops/do-while.ts b/src/transformation/visitors/loops/do-while.ts index 0fafc710a..205fc5252 100644 --- a/src/transformation/visitors/loops/do-while.ts +++ b/src/transformation/visitors/loops/do-while.ts @@ -1,23 +1,39 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { FunctionVisitor } from "../../context"; -import { transformLoopBody } from "./utils"; +import { invertCondition, transformLoopBody } from "./utils"; -export const transformWhileStatement: FunctionVisitor = (statement, context) => - lua.createWhileStatement( - lua.createBlock(transformLoopBody(context, statement)), - context.transformExpression(statement.expression), - statement - ); +export const transformWhileStatement: FunctionVisitor = (statement, context) => { + const body = transformLoopBody(context, statement); + + context.pushPrecedingStatements(); + let condition = context.transformExpression(statement.expression); + const precedingStatements = context.popPrecedingStatements(); + + // Change from 'while condition' to 'while true - if not condition then break' + if (precedingStatements.length > 0) { + precedingStatements.push( + lua.createIfStatement(invertCondition(condition), lua.createBlock([lua.createBreakStatement()])) + ); + body.unshift(...precedingStatements); + condition = lua.createBooleanLiteral(true); + } + + return lua.createWhileStatement(lua.createBlock(body), condition, statement); +}; export const transformDoStatement: FunctionVisitor = (statement, context) => { const body = lua.createDoStatement(transformLoopBody(context, statement)); - let condition = context.transformExpression(statement.expression); - if (lua.isUnaryExpression(condition) && condition.operator === lua.SyntaxKind.NotOperator) { - condition = condition.operand; - } else { - condition = lua.createUnaryExpression(condition, lua.SyntaxKind.NotOperator); + + context.pushPrecedingStatements(); + let condition = invertCondition(context.transformExpression(statement.expression)); + const precedingStatements = context.popPrecedingStatements(); + + // Change from 'repeat until not condition' to 'repeat - if not condition break - until false' + if (precedingStatements.length > 0) { + precedingStatements.push(lua.createIfStatement(condition, lua.createBlock([lua.createBreakStatement()]))); + condition = lua.createBooleanLiteral(false); } - return lua.createRepeatStatement(lua.createBlock([body]), condition, statement); + return lua.createRepeatStatement(lua.createBlock([body, ...precedingStatements]), condition, statement); }; diff --git a/src/transformation/visitors/loops/for.ts b/src/transformation/visitors/loops/for.ts index 98864082e..d5b267b86 100644 --- a/src/transformation/visitors/loops/for.ts +++ b/src/transformation/visitors/loops/for.ts @@ -2,7 +2,7 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { FunctionVisitor } from "../../context"; import { checkVariableDeclarationList, transformVariableDeclaration } from "../variable-declaration"; -import { transformLoopBody } from "./utils"; +import { invertCondition, transformLoopBody } from "./utils"; export const transformForStatement: FunctionVisitor = (statement, context) => { const result: lua.Statement[] = []; @@ -17,13 +17,26 @@ export const transformForStatement: FunctionVisitor = (statemen } } - const condition = statement.condition - ? context.transformExpression(statement.condition) - : lua.createBooleanLiteral(true); - - // Add body const body: lua.Statement[] = transformLoopBody(context, statement); + let condition: lua.Expression; + if (statement.condition) { + context.pushPrecedingStatements(); + condition = context.transformExpression(statement.condition); + const precedingStatements = context.popPrecedingStatements(); + + // Change 'while condition' to 'while true - if not condition break' + if (precedingStatements.length > 0) { + precedingStatements.push( + lua.createIfStatement(invertCondition(condition), lua.createBlock([lua.createBreakStatement()])) + ); + body.unshift(...precedingStatements); + condition = lua.createBooleanLiteral(true); + } + } else { + condition = lua.createBooleanLiteral(true); + } + if (statement.incrementor) { body.push(...context.transformStatements(ts.factory.createExpressionStatement(statement.incrementor))); } diff --git a/src/transformation/visitors/loops/utils.ts b/src/transformation/visitors/loops/utils.ts index a7402b653..cc4d8638d 100644 --- a/src/transformation/visitors/loops/utils.ts +++ b/src/transformation/visitors/loops/utils.ts @@ -71,3 +71,11 @@ export function transformForInitializer( return valueVariable; } + +export function invertCondition(expression: lua.Expression) { + if (lua.isUnaryExpression(expression) && expression.operator === lua.SyntaxKind.NotOperator) { + return expression.operand; + } else { + return lua.createUnaryExpression(expression, lua.SyntaxKind.NotOperator); + } +} diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index c3765017e..d2dd11e71 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -28,6 +28,15 @@ test.each([ `.expectToMatchJsResult(); }); +test.each([true, false])("ternary operator (%p)", condition => { + util.testFunction` + let a = 0, b = 0; + let condition: boolean = ${condition}; + const c = condition ? a++ : b++; + return [a, b, c]; + `.expectToMatchJsResult(); +}); + describe("execution order", () => { const sequenceTests = [ "i++, i", @@ -83,4 +92,60 @@ describe("execution order", () => { return result; `.expectToMatchJsResult(); }); + + test("comma operator", () => { + util.testFunction` + let a = 0, b = 0, c = 0; + const d = (a++, b += a, c += b); + return [a, b, c, d]; + `.expectToMatchJsResult(); + }); +}); + +describe("loop expressions", () => { + test("while loop", () => { + util.testFunction` + let i = 0, j = 0; + while (i++ < 5) { + ++j; + if (j >= 10) { + break; + } + } + return i; + `.expectToMatchJsResult(); + }); + + test("for loop", () => { + util.testFunction` + let i: number, j: number; + for (i = 0, j = 0; i++ < 5 && j < 10; ++j) {} + return i; + `.expectToMatchJsResult(); + }); + + test("do while loop", () => { + util.testFunction` + let i = 0, j = 0; + do { + ++j; + if (j >= 10) { + break; + } + } while (i++ < 5); + return i; + `.expectToMatchJsResult(); + }); + + test("do while loop scoping", () => { + util.testFunction` + let x = 0; + let result = 0; + do { + let x = -10; + ++result; + } while (x++ >= 0 && result < 2); + return result; + `.expectToMatchJsResult(); + }); }); From 4076afd070367ee25308dd3838c9b85fd80ae7db Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 2 Sep 2021 05:32:12 -0600 Subject: [PATCH 06/51] switch test (currently broken) --- test/unit/precedingStatements.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index d2dd11e71..3b9ee3d77 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -149,3 +149,15 @@ describe("loop expressions", () => { `.expectToMatchJsResult(); }); }); + +test("switch scoping", () => { + util.testFunction` + let i = 0; + let x = 0; + switch (x) { + case i++: + return i; + case i++: + } + `.expectToMatchJsResult(); +}); From 7f4542023b73d58884057bf41b54e0b05870840d Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 2 Sep 2021 07:16:28 -0600 Subject: [PATCH 07/51] refactor to expression list transformation --- src/transformation/visitors/call.ts | 74 +---------------- .../visitors/expression-list.ts | 83 +++++++++++++++++++ src/transformation/visitors/literal.ts | 4 +- 3 files changed, 88 insertions(+), 73 deletions(-) create mode 100644 src/transformation/visitors/expression-list.ts diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 38f6abc59..bd49cdb35 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -5,7 +5,7 @@ import { FunctionVisitor, TransformationContext } from "../context"; import { AnnotationKind, getTypeAnnotations, isTupleReturnCall } from "../utils/annotations"; import { validateAssignment } from "../utils/assignment-validation"; import { ContextType, getDeclarationContextType } from "../utils/function-context"; -import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; +import { wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { isValidLuaIdentifier } from "../utils/safe-names"; import { isExpressionWithEvaluationEffect } from "../utils/typescript"; @@ -27,85 +27,17 @@ import { } from "./language-extensions/table"; import { annotationRemoved, invalidTableDeleteExpression, invalidTableSetExpression } from "../utils/diagnostics"; import { transformToImmediatelyInvokedFunctionExpression } from "../utils/transform"; -import { isOptimizedVarArgSpreadElement } from "./spread"; +import { transformExpressionList } from "./expression-list"; export type PropertyCallExpression = ts.CallExpression & { expression: ts.PropertyAccessExpression }; -export function flattenSpreadExpressions( - context: TransformationContext, - expressions: readonly ts.Expression[] -): lua.Expression[] { - const transformedExpressions: lua.Expression[] = []; - const unwrapInConcat: boolean[] = []; - let lastExpressionWithPrecedingStatements = -1; - for (let i = 0; i < expressions.length; ++i) { - context.pushPrecedingStatements(); - const transformedExpression = context.transformExpression(expressions[i]); - const precedingStatements = context.popPrecedingStatements(); - - // If preceding statements were generated, walk back and cache previous values in temps - if (precedingStatements.length > 0) { - for (let j = lastExpressionWithPrecedingStatements + 1; j < i; ++j) { - let previousExpression = transformedExpressions[j]; - if (!lua.isLiteral(previousExpression)) { - const tempVar = lua.createIdentifier(context.createTempNameFromExpression(previousExpression)); - if (ts.isSpreadElement(expressions[j])) { - previousExpression = wrapInTable(previousExpression); - unwrapInConcat[j] = true; - } - context.addPrecedingStatements([ - lua.createVariableDeclarationStatement(tempVar, previousExpression), - ]); - transformedExpressions[j] = lua.cloneIdentifier(tempVar); - } - } - lastExpressionWithPrecedingStatements = i; - - // Bubble up preceding statements - context.addPrecedingStatements(precedingStatements); - } - - transformedExpressions.push(transformedExpression); - unwrapInConcat.push(false); - } - - // If there are spreads in the middle, use the array concat lib function - const firstSpreadIndex = expressions.findIndex( - e => ts.isSpreadElement(e) && !isOptimizedVarArgSpreadElement(context, e) - ); - if (firstSpreadIndex >= 0 && firstSpreadIndex < expressions.length - 1) { - const tbls: lua.Expression[] = []; - let tbl: lua.Expression[] = []; - for (let i = 0; i < expressions.length; ++i) { - let transformedExpression = transformedExpressions[i]; - if (ts.isSpreadElement(expressions[i])) { - if (unwrapInConcat[i]) { - transformedExpression = createUnpackCall(context, transformedExpression); - } - tbls.push(wrapInTable(...tbl, transformedExpression)); - tbl = []; - } else { - tbl.push(transformedExpression); - } - } - if (tbl.length > 0) { - tbls.push(wrapInTable(...tbl)); - } - return [ - createUnpackCall(context, transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...tbls)), - ]; - } - - return transformedExpressions; -} - export function transformArguments( context: TransformationContext, params: readonly ts.Expression[], signature?: ts.Signature, callContext?: ts.Expression ): lua.Expression[] { - const parameters = flattenSpreadExpressions(context, params); + const parameters = transformExpressionList(context, params); // Add context as first param if present if (callContext) { diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts new file mode 100644 index 000000000..15171dd39 --- /dev/null +++ b/src/transformation/visitors/expression-list.ts @@ -0,0 +1,83 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { isOptimizedVarArgSpreadElement } from "./spread"; +import { assert } from "../../utils"; +import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; + +// Transforms a list of expressions while flattening spreads and maintaining execution order +export function transformExpressionList( + context: TransformationContext, + expressions: readonly ts.Expression[] +): lua.Expression[] { + // Transform expressions and collect info about them + let lastPrecedingStatementsIndex = -1; + let firstSpreadIndex = -1; + const transformedExpressionInfo = expressions.map((expression, i) => { + context.pushPrecedingStatements(); + const transformedExpression = context.transformExpression(expression); + const precedingStatements = context.popPrecedingStatements(); + + if (precedingStatements.length > 0) lastPrecedingStatementsIndex = i; + + const isSpread = ts.isSpreadElement(expression) && !isOptimizedVarArgSpreadElement(context, expression); + if (isSpread && firstSpreadIndex === -1) firstSpreadIndex = i; + + return { transformedExpression, precedingStatements, isSpread }; + }); + + // If there are preceding statements, cache expressions in temps to maintain execution order + if (lastPrecedingStatementsIndex >= 0) { + for (let i = 0; i < transformedExpressionInfo.length; ++i) { + const expressionInfo = transformedExpressionInfo[i]; + if ( + i < lastPrecedingStatementsIndex && + !lua.isLiteral(expressionInfo.transformedExpression) && + expressionInfo.precedingStatements.length === 0 + ) { + const tempVar = lua.createIdentifier( + context.createTempNameFromExpression(expressionInfo.transformedExpression) + ); + let expression = expressionInfo.transformedExpression; + let tempExpression: lua.Expression = lua.cloneIdentifier(tempVar); + + // Spreads: strip unpack from original expression and add it to the temp's evaluation + if (expressionInfo.isSpread) { + assert(lua.isCallExpression(expression) && expression.params.length === 1); + expression = expression.params[0]; + tempExpression = createUnpackCall(context, tempExpression); + } + + // Inject temp assignment in correct place in preceding statements + context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, expression)]); + expressionInfo.transformedExpression = tempExpression; + } + + // Bubble up preceding statements + context.addPrecedingStatements(expressionInfo.precedingStatements); + } + } + + // If there are spreads in the middle, use the array concat lib function + if (firstSpreadIndex >= 0 && firstSpreadIndex < expressions.length - 1) { + const tbls: lua.Expression[] = []; + let tbl: lua.Expression[] = []; + for (const expressionInfo of transformedExpressionInfo) { + if (expressionInfo.isSpread) { + tbls.push(wrapInTable(...tbl, expressionInfo.transformedExpression)); + tbl = []; + } else { + tbl.push(expressionInfo.transformedExpression); + } + } + if (tbl.length > 0) { + tbls.push(wrapInTable(...tbl)); + } + return [ + createUnpackCall(context, transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...tbls)), + ]; + } + + return transformedExpressionInfo.map(e => e.transformedExpression); +} diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 316d34613..acb19eebb 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -9,7 +9,7 @@ import { createSafeName, hasUnsafeIdentifierName, hasUnsafeSymbolName } from ".. import { getSymbolIdOfSymbol, trackSymbolReference } from "../utils/symbols"; import { isArrayType } from "../utils/typescript"; import { transformFunctionLikeDeclaration } from "./function"; -import { flattenSpreadExpressions } from "./call"; +import { transformExpressionList } from "./expression-list"; import { findMultiAssignmentViolations } from "./language-extensions/multi"; import { formatJSXStringValueLiteral } from "./jsx/jsx"; @@ -200,7 +200,7 @@ const transformArrayLiteralExpression: FunctionVisitor ts.isOmittedExpression(e) ? ts.factory.createIdentifier("undefined") : e ); - const values = flattenSpreadExpressions(context, filteredElements).map(e => lua.createTableFieldExpression(e)); + const values = transformExpressionList(context, filteredElements).map(e => lua.createTableFieldExpression(e)); return lua.createTableExpression(values, expression); }; From 52714ef19a0895983c981a4c62a0faf10b99c987 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 06:49:13 -0600 Subject: [PATCH 08/51] more refactoring and fixes for expression lists --- .../visitors/expression-list.ts | 139 +++++++++++------- test/unit/precedingStatements.spec.ts | 4 + 2 files changed, 86 insertions(+), 57 deletions(-) diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 15171dd39..f94096d3b 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -1,11 +1,82 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; -import { isOptimizedVarArgSpreadElement } from "./spread"; +// import { isOptimizedVarArgSpreadElement } from "./spread"; import { assert } from "../../utils"; import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +interface ExpressionListInfo { + transformedExpression: lua.Expression; + precedingStatements: lua.Statement[]; + isSpread: boolean; + needsUnpack?: boolean; +} + +function isPrecedingStatementTemp(info: ExpressionListInfo) { + return info.precedingStatements.length > 0 && lua.isIdentifier(info.transformedExpression); +} + +function cacheExpressionsInTemps( + context: TransformationContext, + expressionInfo: ExpressionListInfo[], + lastPrecedingStatementsIndex: number +) { + for (let i = 0; i < expressionInfo.length; ++i) { + const info = expressionInfo[i]; + + // Bubble up preceding statements + context.addPrecedingStatements(info.precedingStatements); + + // Only cache expressions in front of the last one that created preceding statements + if (i > lastPrecedingStatementsIndex) continue; + + // Simple literals can't be affected by anything, so no need to cache them + if (lua.isLiteral(info.transformedExpression)) continue; + + // If expression is just a temp result for other preceding statements, no need to cache + if (isPrecedingStatementTemp(info)) continue; + + // Strip 'unpack' from spreads - we'll add it back later in buildArrayConcatCall + let expression = info.transformedExpression; + if (info.isSpread) { + assert(lua.isCallExpression(expression) && expression.params.length === 1); + expression = expression.params[0]; + info.needsUnpack = true; + } + + // Inject temp assignment in correct place in preceding statements + const tempVar = lua.createIdentifier(context.createTempNameFromExpression(info.transformedExpression)); + context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, expression)]); + info.transformedExpression = lua.cloneIdentifier(tempVar); + } +} + +function buildArrayConcatCall(context: TransformationContext, expressionInfo: ExpressionListInfo[]) { + const tbls: lua.Expression[] = []; + let tbl: lua.Expression[] = []; + for (const info of expressionInfo) { + if (info.isSpread) { + if (info.needsUnpack) { + if (tbl.length === 0) { + tbls.push(info.transformedExpression); + } else { + tbls.push(wrapInTable(...tbl, createUnpackCall(context, info.transformedExpression))); + } + } else { + tbls.push(wrapInTable(...tbl, info.transformedExpression)); + } + tbl = []; + } else { + tbl.push(info.transformedExpression); + } + } + if (tbl.length > 0) { + tbls.push(wrapInTable(...tbl)); + } + return [createUnpackCall(context, transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...tbls))]; +} + // Transforms a list of expressions while flattening spreads and maintaining execution order export function transformExpressionList( context: TransformationContext, @@ -13,71 +84,25 @@ export function transformExpressionList( ): lua.Expression[] { // Transform expressions and collect info about them let lastPrecedingStatementsIndex = -1; - let firstSpreadIndex = -1; - const transformedExpressionInfo = expressions.map((expression, i) => { + const transformListExpression = (expression: ts.Expression, index: number): ExpressionListInfo => { context.pushPrecedingStatements(); const transformedExpression = context.transformExpression(expression); const precedingStatements = context.popPrecedingStatements(); - - if (precedingStatements.length > 0) lastPrecedingStatementsIndex = i; - - const isSpread = ts.isSpreadElement(expression) && !isOptimizedVarArgSpreadElement(context, expression); - if (isSpread && firstSpreadIndex === -1) firstSpreadIndex = i; - - return { transformedExpression, precedingStatements, isSpread }; - }); + if (precedingStatements.length > 0) lastPrecedingStatementsIndex = index; + return { transformedExpression, precedingStatements, isSpread: ts.isSpreadElement(expression) }; + }; + const expressionInfo = expressions.map(transformListExpression); // If there are preceding statements, cache expressions in temps to maintain execution order if (lastPrecedingStatementsIndex >= 0) { - for (let i = 0; i < transformedExpressionInfo.length; ++i) { - const expressionInfo = transformedExpressionInfo[i]; - if ( - i < lastPrecedingStatementsIndex && - !lua.isLiteral(expressionInfo.transformedExpression) && - expressionInfo.precedingStatements.length === 0 - ) { - const tempVar = lua.createIdentifier( - context.createTempNameFromExpression(expressionInfo.transformedExpression) - ); - let expression = expressionInfo.transformedExpression; - let tempExpression: lua.Expression = lua.cloneIdentifier(tempVar); - - // Spreads: strip unpack from original expression and add it to the temp's evaluation - if (expressionInfo.isSpread) { - assert(lua.isCallExpression(expression) && expression.params.length === 1); - expression = expression.params[0]; - tempExpression = createUnpackCall(context, tempExpression); - } - - // Inject temp assignment in correct place in preceding statements - context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, expression)]); - expressionInfo.transformedExpression = tempExpression; - } - - // Bubble up preceding statements - context.addPrecedingStatements(expressionInfo.precedingStatements); - } + cacheExpressionsInTemps(context, expressionInfo, lastPrecedingStatementsIndex); } // If there are spreads in the middle, use the array concat lib function - if (firstSpreadIndex >= 0 && firstSpreadIndex < expressions.length - 1) { - const tbls: lua.Expression[] = []; - let tbl: lua.Expression[] = []; - for (const expressionInfo of transformedExpressionInfo) { - if (expressionInfo.isSpread) { - tbls.push(wrapInTable(...tbl, expressionInfo.transformedExpression)); - tbl = []; - } else { - tbl.push(expressionInfo.transformedExpression); - } - } - if (tbl.length > 0) { - tbls.push(wrapInTable(...tbl)); - } - return [ - createUnpackCall(context, transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...tbls)), - ]; + const firstSpreadIndex = expressionInfo.findIndex(i => i.isSpread); + if (firstSpreadIndex >= 0 && firstSpreadIndex < expressionInfo.length - 1) { + return buildArrayConcatCall(context, expressionInfo); } - return transformedExpressionInfo.map(e => e.transformedExpression); + return expressionInfo.map(e => e.transformedExpression); } diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 3b9ee3d77..88bda7014 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -53,12 +53,15 @@ describe("execution order", () => { "i, ...[1, i++, 2], i++", "i, ...[1, i++, 2], i++, ...[3, i++, 4]", "i, ...a, i++, ...[1, i++, 2], i, i++, ...a", + "i, inc(), i++", + "i, ...[1, i++, inc(), 2], i++", ]; test.each(sequenceTests)("array literal ([%p])", sequence => { util.testFunction` const a = [7, 8, 9]; let i = 0; + function inc() { ++i; return i; } return [${sequence}]; `.expectToMatchJsResult(); }); @@ -67,6 +70,7 @@ describe("execution order", () => { util.testFunction` const a = [7, 8, 9]; let i = 0; + function inc() { ++i; return i; } function foo(...args: unknown[]) { return args; } return foo(${sequence}); `.expectToMatchJsResult(); From 306a5b9c61ea8f312b8886d056408611849f1eee Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 07:10:26 -0600 Subject: [PATCH 09/51] refactored object literals a bit --- .../visitors/expression-list.ts | 3 +- src/transformation/visitors/literal.ts | 47 +++++++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index f94096d3b..bf0889f88 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -1,7 +1,6 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; -// import { isOptimizedVarArgSpreadElement } from "./spread"; import { assert } from "../../utils"; import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; @@ -29,7 +28,7 @@ function cacheExpressionsInTemps( context.addPrecedingStatements(info.precedingStatements); // Only cache expressions in front of the last one that created preceding statements - if (i > lastPrecedingStatementsIndex) continue; + if (i >= lastPrecedingStatementsIndex) continue; // Simple literals can't be affected by anything, so no need to cache them if (lua.isLiteral(info.transformedExpression)) continue; diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index acb19eebb..7c66cb0ce 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -72,6 +72,7 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor 0) { + lastPrecedingStatementsIndex = i; } + } + + // Expressions referenced before others that produced preceding statements need to be cached in temps + if (lastPrecedingStatementsIndex >= 0) { + for (let i = 0; i < transformedProperties.length; ++i) { + const property = transformedProperties[i]; + + const propertyPrecedingStatements = precedingStatements[i]; + context.addPrecedingStatements(propertyPrecedingStatements); - // If preceding statements were generated, walk back and cache previous values in temps - for (let j = lastPrecedingStatementsIndex + 1; j < i; ++j) { - const previousProperty = transformedProperties[j]; - if (lua.isTableFieldExpression(previousProperty)) { - if (!lua.isLiteral(previousProperty.value)) { - const tempVar = lua.createIdentifier( - context.createTempNameFromExpression(previousProperty.value) - ); + if (i >= lastPrecedingStatementsIndex) continue; + + if (lua.isTableFieldExpression(property)) { + if ( + !lua.isLiteral(property.value) && + !(propertyPrecedingStatements.length > 0 && lua.isIdentifier(property.value)) + ) { + const tempVar = lua.createIdentifier(context.createTempNameFromExpression(property.value)); context.addPrecedingStatements([ - lua.createVariableDeclarationStatement(tempVar, previousProperty.value), + lua.createVariableDeclarationStatement(tempVar, property.value), ]); - previousProperty.value = lua.cloneIdentifier(tempVar); + property.value = lua.cloneIdentifier(tempVar); } } else { - const tempVar = lua.createIdentifier(context.createTempNameFromExpression(previousProperty)); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, previousProperty)]); - transformedProperties[j] = lua.cloneIdentifier(tempVar); + const tempVar = lua.createIdentifier(context.createTempNameFromExpression(property)); + context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, property)]); + transformedProperties[i] = lua.cloneIdentifier(tempVar); } } - lastPrecedingStatementsIndex = i; - - // Bubble up preceding statements - context.addPrecedingStatements(precedingStatements); } // Sort into field expressions and tables to pass into __TS__ObjectAssign From 6945c7e7c75ebc64bb1ce3124fe64c822f92ea1f Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 09:56:23 -0600 Subject: [PATCH 10/51] refactorings, including removal of old iife stuff --- src/transformation/context/context.ts | 25 +++++++++---- src/transformation/utils/lua-ast.ts | 20 ---------- src/transformation/utils/transform.ts | 24 ------------ .../visitors/binary-expression/assignments.ts | 34 ++++++----------- .../visitors/binary-expression/compound.ts | 24 +++++------- .../visitors/binary-expression/index.ts | 2 +- src/transformation/visitors/call.ts | 15 ++------ src/transformation/visitors/class/index.ts | 17 +++------ .../visitors/expression-list.ts | 37 +++++++++++-------- src/transformation/visitors/function.ts | 22 ++--------- src/transformation/visitors/literal.ts | 4 +- 11 files changed, 75 insertions(+), 149 deletions(-) delete mode 100644 src/transformation/utils/transform.ts diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index ac40f31a4..8c0df27f7 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -140,22 +140,33 @@ export class TransformationContext { } } - public createTempName(prefix = "") { - return `____${prefix}${this.nextTempId++}`; + public createTempName(prefix = "temp") { + return `____${prefix}_${this.nextTempId++}`; } - public createTempNameFromExpression(expression: lua.Expression) { + public createTempForLuaExpression(expression: lua.Expression) { let name: string | undefined; if (lua.isStringLiteral(expression)) { name = expression.value; } else if (lua.isIdentifier(expression)) { name = expression.text; } - if (!name) { - name = "temp"; - } else if (!isValidLuaIdentifier(name)) { + if (name && !isValidLuaIdentifier(name)) { name = fixInvalidLuaIdentifier(name); } - return `____${name}${this.nextTempId++}`; + const identifier = lua.createIdentifier(this.createTempName(name)); + lua.setNodePosition(identifier, lua.getOriginalPos(expression)); + return identifier; + } + + public createTempForExpression(expression: ts.Expression) { + let name: string | undefined; + if (ts.isStringLiteral(expression) || ts.isIdentifier(expression)) { + name = expression.text; + if (!isValidLuaIdentifier(name)) { + name = fixInvalidLuaIdentifier(name); + } + } + return lua.createIdentifier(this.createTempName(name), expression); } } diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index 2e87f4c0d..33f31ddae 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -62,26 +62,6 @@ export function getNumberLiteralValue(expression?: lua.Expression) { return undefined; } -// Prefer use of transformToImmediatelyInvokedFunctionExpression to maintain correct scope. If you use this directly, -// ensure you push/pop a function scope appropriately to avoid incorrect vararg optimization. -export function createImmediatelyInvokedFunctionExpression( - scope: Scope, - statements: lua.Statement[], - result: lua.Expression | lua.Expression[], - tsOriginal?: ts.Node -): [lua.Statement[], lua.Expression] { - const resultName = `____result${scope.id}`; - const resultIdentifier = lua.createIdentifier(resultName, tsOriginal); - const body = [...statements, lua.createAssignmentStatement(resultIdentifier, result, tsOriginal)]; - return [ - [ - lua.createVariableDeclarationStatement(lua.cloneIdentifier(resultIdentifier), undefined, tsOriginal), - lua.createDoStatement(body, tsOriginal), - ], - lua.cloneIdentifier(resultIdentifier), - ]; -} - export function createUnpackCall( context: TransformationContext, expression: lua.Expression, diff --git a/src/transformation/utils/transform.ts b/src/transformation/utils/transform.ts deleted file mode 100644 index fa1280ae7..000000000 --- a/src/transformation/utils/transform.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as ts from "typescript"; -import * as lua from "../../LuaAST"; -import { castArray } from "../../utils"; -import { TransformationContext } from "../context"; -import { createImmediatelyInvokedFunctionExpression } from "./lua-ast"; -import { ScopeType, pushScope, popScope } from "./scope"; - -export interface ImmediatelyInvokedFunctionParameters { - statements: lua.Statement | lua.Statement[]; - result: lua.Expression | lua.Expression[]; -} - -export function transformToImmediatelyInvokedFunctionExpression( - context: TransformationContext, - transformFunction: () => ImmediatelyInvokedFunctionParameters, - tsOriginal?: ts.Node -): lua.Expression { - const scope = pushScope(context, ScopeType.Block); - let { statements, result } = transformFunction(); - [statements, result] = createImmediatelyInvokedFunctionExpression(scope, castArray(statements), result, tsOriginal); - context.addPrecedingStatements(statements); - popScope(context); - return result; -} diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index b934ca2e0..64a71f890 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -11,10 +11,6 @@ import { transformElementAccessArgument } from "../access"; import { isArrayLength, transformDestructuringAssignment } from "./destructuring-assignments"; import { isMultiReturnCall } from "../language-extensions/multi"; import { popScope, pushScope, ScopeType } from "../../utils/scope"; -import { - ImmediatelyInvokedFunctionParameters, - transformToImmediatelyInvokedFunctionExpression, -} from "../../utils/transform"; import { notAllowedOptionalAssignment } from "../../utils/diagnostics"; export function transformAssignmentLeftHandSideExpression( @@ -78,8 +74,8 @@ export function transformAssignment( function transformDestructuredAssignmentExpression( context: TransformationContext, expression: ts.DestructuringAssignment -): ImmediatelyInvokedFunctionParameters { - const rootIdentifier = lua.createAnonymousIdentifier(expression.left); +) { + const rootIdentifier = context.createTempForExpression(expression.right); let right = context.transformExpression(expression.right); if (isMultiReturnCall(context, expression.right)) { @@ -115,11 +111,9 @@ export function transformAssignmentExpression( } if (isDestructuringAssignment(expression)) { - return transformToImmediatelyInvokedFunctionExpression( - context, - () => transformDestructuredAssignmentExpression(context, expression), - expression - ); + const { statements, result } = transformDestructuredAssignmentExpression(context, expression); + context.addPrecedingStatements(statements); + return result; } if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) { @@ -153,18 +147,12 @@ export function transformAssignmentExpression( popScope(context); return lua.createCallExpression(iife, args, expression); } else { - return transformToImmediatelyInvokedFunctionExpression( - context, - () => { - // Simple assignment - // (function() ${left} = ${right}; return ${left} end)() - const left = context.transformExpression(expression.left); - const right = context.transformExpression(expression.right); - const statements = transformAssignment(context, expression.left, right); - return { statements, result: left }; - }, - expression - ); + // Simple assignment + // ${left} = ${right}; return ${left} + const left = context.transformExpression(expression.left); + const right = context.transformExpression(expression.right); + context.addPrecedingStatements(transformAssignment(context, expression.left, right)); + return left; } } diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index eb01502ac..2186ba76c 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -2,10 +2,6 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { cast, assertNever } from "../../../utils"; import { TransformationContext } from "../../context"; -import { - ImmediatelyInvokedFunctionParameters, - transformToImmediatelyInvokedFunctionExpression, -} from "../../utils/transform"; import { isArrayType, isExpressionWithEvaluationEffect } from "../../utils/typescript"; import { transformBinaryOperation } from "../binary-expression"; import { transformAssignment } from "./assignments"; @@ -85,7 +81,7 @@ export function transformCompoundAssignment( rhs: ts.Expression, operator: CompoundAssignmentToken, isPostfix: boolean -): ImmediatelyInvokedFunctionParameters { +) { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); context.pushPrecedingStatements(); const right = context.transformExpression(rhs); @@ -95,15 +91,15 @@ export function transformCompoundAssignment( if (objExpression && indexExpression) { // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects // local __obj, __index = ${objExpression}, ${indexExpression}; - const obj = lua.createIdentifier("____obj"); - const index = lua.createIdentifier("____index"); + const obj = context.createTempForExpression(objExpression); + const index = context.createTempForExpression(indexExpression); const objAndIndexDeclaration = lua.createVariableDeclarationStatement( [obj, index], [context.transformExpression(objExpression), context.transformExpression(indexExpression)] ); const accessExpression = lua.createTableIndexExpression(obj, index); - const tmp = lua.createIdentifier("____tmp"); + const tmp = context.createTempForLuaExpression(left); let tmpDeclaration: lua.VariableDeclarationStatement; let assignStatement: lua.AssignmentStatement; if (isPostfix) { @@ -127,7 +123,7 @@ export function transformCompoundAssignment( // local ____tmp = ${left}; // ${left} = ____tmp ${replacementOperator} ${right}; // return ____tmp - const tmpIdentifier = lua.createIdentifier("____tmp"); + const tmpIdentifier = context.createTempForLuaExpression(left); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); const operatorExpression = transformBinaryOperation(context, tmpIdentifier, right, operator, expression); const assignStatements = transformAssignment(context, lhs, operatorExpression); @@ -138,7 +134,7 @@ export function transformCompoundAssignment( // local ____tmp = ${left} ${replacementOperator} ${right}; // ${left} = ____tmp; // return ____tmp - const tmpIdentifier = lua.createIdentifier("____tmp"); + const tmpIdentifier = context.createTempForLuaExpression(left); const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, operatorExpression); const assignStatements = transformAssignment(context, lhs, tmpIdentifier); @@ -180,11 +176,9 @@ export function transformCompoundAssignmentExpression( operator: CompoundAssignmentToken, isPostfix: boolean ): lua.Expression { - return transformToImmediatelyInvokedFunctionExpression( - context, - () => transformCompoundAssignment(context, expression, lhs, rhs, operator, isPostfix), - expression - ); + const { statements, result } = transformCompoundAssignment(context, expression, lhs, rhs, operator, isPostfix); + context.addPrecedingStatements(Array.isArray(statements) ? statements : [statements]); + return result; } export function transformCompoundAssignmentStatement( diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 3fc026d79..1d2ba5e4a 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -86,7 +86,7 @@ function createShortCircuitBinaryExpression( const rhs = context.transformExpression(node.right); const rightPrecedingStatements = context.popPrecedingStatements(); if (rightPrecedingStatements.length > 0) { - const result = lua.createIdentifier(context.createTempNameFromExpression(lhs)); + const result = context.createTempForLuaExpression(lhs); const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs); const ifStatement = lua.createIfStatement( createCondition(lua.cloneIdentifier(result)), diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index bd49cdb35..456f3fea1 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -26,7 +26,6 @@ import { transformTableSetExpression, } from "./language-extensions/table"; import { annotationRemoved, invalidTableDeleteExpression, invalidTableSetExpression } from "../utils/diagnostics"; -import { transformToImmediatelyInvokedFunctionExpression } from "../utils/transform"; import { transformExpressionList } from "./expression-list"; export type PropertyCallExpression = ts.CallExpression & { expression: ts.PropertyAccessExpression }; @@ -204,11 +203,8 @@ export const transformCallExpression: FunctionVisitor = (node if (isTableDeleteCall(context, node)) { context.diagnostics.push(invalidTableDeleteExpression(node)); - return transformToImmediatelyInvokedFunctionExpression( - context, - () => ({ statements: transformTableDeleteExpression(context, node), result: lua.createNilLiteral() }), - node - ); + context.addPrecedingStatements([transformTableDeleteExpression(context, node)]); + return lua.createNilLiteral(); } if (isTableGetCall(context, node)) { @@ -221,11 +217,8 @@ export const transformCallExpression: FunctionVisitor = (node if (isTableSetCall(context, node)) { context.diagnostics.push(invalidTableSetExpression(node)); - return transformToImmediatelyInvokedFunctionExpression( - context, - () => ({ statements: transformTableSetExpression(context, node), result: lua.createNilLiteral() }), - node - ); + context.addPrecedingStatements([transformTableSetExpression(context, node)]); + return lua.createNilLiteral(); } if (ts.isPropertyAccessExpression(node.expression)) { diff --git a/src/transformation/visitors/class/index.ts b/src/transformation/visitors/class/index.ts index 98d74f84a..d0c8afede 100644 --- a/src/transformation/visitors/class/index.ts +++ b/src/transformation/visitors/class/index.ts @@ -10,9 +10,8 @@ import { hasDefaultExportModifier, isSymbolExported, } from "../../utils/export"; -import { createSelfIdentifier, unwrapVisitorResult } from "../../utils/lua-ast"; +import { createSelfIdentifier } from "../../utils/lua-ast"; import { createSafeName, isUnsafeName } from "../../utils/safe-names"; -import { transformToImmediatelyInvokedFunctionExpression } from "../../utils/transform"; import { transformIdentifier } from "../identifier"; import { createDecoratingExpression, transformDecoratorExpression } from "./decorators"; import { transformAccessorDeclarations } from "./members/accessors"; @@ -45,14 +44,9 @@ export function transformClassAsExpression( expression: ts.ClassLikeDeclaration, context: TransformationContext ): lua.Expression { - return transformToImmediatelyInvokedFunctionExpression( - context, - () => { - const { statements, name } = transformClassLikeDeclaration(expression, context); - return { statements: unwrapVisitorResult(statements), result: name }; - }, - expression - ); + const { statements, name } = transformClassLikeDeclaration(expression, context); + context.addPrecedingStatements(statements); + return name; } const classSuperInfos = new WeakMap(); @@ -72,8 +66,7 @@ function transformClassLikeDeclaration( } else if (classDeclaration.name !== undefined) { className = transformIdentifier(context, classDeclaration.name); } else { - // TypeScript error - className = lua.createAnonymousIdentifier(); + className = lua.createIdentifier(context.createTempName("class")); } const annotations = getTypeAnnotations(context.checker.getTypeAtLocation(classDeclaration)); diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index bf0889f88..9d4ad7eeb 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -16,27 +16,34 @@ function isPrecedingStatementTemp(info: ExpressionListInfo) { return info.precedingStatements.length > 0 && lua.isIdentifier(info.transformedExpression); } -function cacheExpressionsInTemps( +function processPrecedingStatements( context: TransformationContext, expressionInfo: ExpressionListInfo[], lastPrecedingStatementsIndex: number ) { + if (lastPrecedingStatementsIndex < 0) { + return; + } + for (let i = 0; i < expressionInfo.length; ++i) { const info = expressionInfo[i]; // Bubble up preceding statements context.addPrecedingStatements(info.precedingStatements); - // Only cache expressions in front of the last one that created preceding statements - if (i >= lastPrecedingStatementsIndex) continue; - - // Simple literals can't be affected by anything, so no need to cache them - if (lua.isLiteral(info.transformedExpression)) continue; - - // If expression is just a temp result for other preceding statements, no need to cache - if (isPrecedingStatementTemp(info)) continue; + // Cache expression in temp to maintain execution order, unless: + // - Expression is after the last one in the list which generated preceding statements + // - Expression is a literal that wouldn't be affected by preceding statements (includes optimized vararg '...') + // - Expression is a temp identifier which is a result of preceding statements + if ( + i >= lastPrecedingStatementsIndex || + lua.isLiteral(info.transformedExpression) || + isPrecedingStatementTemp(info) + ) { + continue; + } - // Strip 'unpack' from spreads - we'll add it back later in buildArrayConcatCall + // Strip 'unpack' from spreads - it will be added back later in buildArrayConcatCall let expression = info.transformedExpression; if (info.isSpread) { assert(lua.isCallExpression(expression) && expression.params.length === 1); @@ -45,7 +52,7 @@ function cacheExpressionsInTemps( } // Inject temp assignment in correct place in preceding statements - const tempVar = lua.createIdentifier(context.createTempNameFromExpression(info.transformedExpression)); + const tempVar = context.createTempForLuaExpression(info.transformedExpression); context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, expression)]); info.transformedExpression = lua.cloneIdentifier(tempVar); } @@ -58,7 +65,7 @@ function buildArrayConcatCall(context: TransformationContext, expressionInfo: Ex if (info.isSpread) { if (info.needsUnpack) { if (tbl.length === 0) { - tbls.push(info.transformedExpression); + tbls.push(info.transformedExpression); // Optimize '{table.unpack(x)}' to just 'x' } else { tbls.push(wrapInTable(...tbl, createUnpackCall(context, info.transformedExpression))); } @@ -92,10 +99,8 @@ export function transformExpressionList( }; const expressionInfo = expressions.map(transformListExpression); - // If there are preceding statements, cache expressions in temps to maintain execution order - if (lastPrecedingStatementsIndex >= 0) { - cacheExpressionsInTemps(context, expressionInfo, lastPrecedingStatementsIndex); - } + // Bubble up preceding statements, generating temps when needed to maintain execution order + processPrecedingStatements(context, expressionInfo, lastPrecedingStatementsIndex); // If there are spreads in the middle, use the array concat lib function const firstSpreadIndex = expressionInfo.findIndex(i => i.isSpread); diff --git a/src/transformation/visitors/function.ts b/src/transformation/visitors/function.ts index c65a9210d..1ada847eb 100644 --- a/src/transformation/visitors/function.ts +++ b/src/transformation/visitors/function.ts @@ -8,7 +8,6 @@ import { createDefaultExportStringLiteral, hasDefaultExportModifier } from "../u import { ContextType, getFunctionContextType } from "../utils/function-context"; import { createExportsIdentifier, - createImmediatelyInvokedFunctionExpression, createLocalOrExportedOrGlobalDeclaration, createSelfIdentifier, wrapInTable, @@ -244,26 +243,13 @@ export function transformFunctionLikeDeclaration( nodes.some(n => context.checker.getSymbolAtLocation(n)?.valueDeclaration === symbol.valueDeclaration) ); - // Only wrap if the name is actually referenced inside the function + // Only handle if the name is actually referenced inside the function if (isReferenced) { const nameIdentifier = transformIdentifier(context, node.name); - // We cannot use transformToImmediatelyInvokedFunctionExpression() here because we need to transpile - // the function first to determine if it's self-referencing. Fortunately, this does not cause issues - // with var-arg optimization because the IIFE is just wrapping another function which will already push - // another scope. - const scope = pushScope(context, ScopeType.Block); - let statements: lua.Statement[] = [ + context.addPrecedingStatements([ lua.createVariableDeclarationStatement(nameIdentifier, functionExpression), - ]; - let result: lua.Expression = lua.cloneIdentifier(nameIdentifier); - [statements, result] = createImmediatelyInvokedFunctionExpression( - scope, - [lua.createVariableDeclarationStatement(nameIdentifier, functionExpression)], - lua.cloneIdentifier(nameIdentifier) - ); - popScope(context); - context.addPrecedingStatements(statements); - return result; + ]); + return lua.cloneIdentifier(nameIdentifier); } } } diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 7c66cb0ce..4c9c5c31d 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -154,14 +154,14 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor 0 && lua.isIdentifier(property.value)) ) { - const tempVar = lua.createIdentifier(context.createTempNameFromExpression(property.value)); + const tempVar = context.createTempForLuaExpression(property.value); context.addPrecedingStatements([ lua.createVariableDeclarationStatement(tempVar, property.value), ]); property.value = lua.cloneIdentifier(tempVar); } } else { - const tempVar = lua.createIdentifier(context.createTempNameFromExpression(property)); + const tempVar = context.createTempForLuaExpression(property); context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, property)]); transformedProperties[i] = lua.cloneIdentifier(tempVar); } From 70d463b207f026d07758c278297538d9c8694b8f Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 10:00:02 -0600 Subject: [PATCH 11/51] snapshot updates --- .../__snapshots__/expressions.spec.ts.snap | 180 ++++++------------ .../__snapshots__/deprecated.spec.ts.snap | 10 +- .../__snapshots__/classes.spec.ts.snap | 6 +- .../__snapshots__/iterable.spec.ts.snap | 16 +- .../__snapshots__/multi.spec.ts.snap | 14 +- .../__snapshots__/table.spec.ts.snap | 62 ++---- 6 files changed, 96 insertions(+), 192 deletions(-) diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 03bb46230..36a0cb24e 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -46,10 +46,8 @@ exports[`Bitop [5.1] ("~a"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitw exports[`Bitop [5.1] ("a&=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.band(a, b) - return a -end)() +a = bit.band(a, b) +____exports.__result = a return ____exports" `; @@ -65,10 +63,8 @@ exports[`Bitop [5.1] ("a&b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bit exports[`Bitop [5.1] ("a<<=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.lshift(a, b) - return a -end)() +a = bit.lshift(a, b) +____exports.__result = a return ____exports" `; @@ -84,10 +80,8 @@ exports[`Bitop [5.1] ("a<>=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.arshift(a, b) - return a -end)() +a = bit.arshift(a, b) +____exports.__result = a return ____exports" `; @@ -95,10 +89,8 @@ exports[`Bitop [5.1] ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: B exports[`Bitop [5.1] ("a>>>=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.rshift(a, b) - return a -end)() +a = bit.rshift(a, b) +____exports.__result = a return ____exports" `; @@ -122,10 +114,8 @@ exports[`Bitop [5.1] ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bi exports[`Bitop [5.1] ("a^=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.bxor(a, b) - return a -end)() +a = bit.bxor(a, b) +____exports.__result = a return ____exports" `; @@ -141,10 +131,8 @@ exports[`Bitop [5.1] ("a^b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bit exports[`Bitop [5.1] ("a|=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.bor(a, b) - return a -end)() +a = bit.bor(a, b) +____exports.__result = a return ____exports" `; @@ -166,10 +154,8 @@ return ____exports" exports[`Bitop [5.2] ("a&=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit32.band(a, b) - return a -end)() +a = bit32.band(a, b) +____exports.__result = a return ____exports" `; @@ -181,10 +167,8 @@ return ____exports" exports[`Bitop [5.2] ("a<<=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit32.lshift(a, b) - return a -end)() +a = bit32.lshift(a, b) +____exports.__result = a return ____exports" `; @@ -196,19 +180,15 @@ return ____exports" exports[`Bitop [5.2] ("a>>=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit32.arshift(a, b) - return a -end)() +a = bit32.arshift(a, b) +____exports.__result = a return ____exports" `; exports[`Bitop [5.2] ("a>>>=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit32.rshift(a, b) - return a -end)() +a = bit32.rshift(a, b) +____exports.__result = a return ____exports" `; @@ -226,10 +206,8 @@ return ____exports" exports[`Bitop [5.2] ("a^=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit32.bxor(a, b) - return a -end)() +a = bit32.bxor(a, b) +____exports.__result = a return ____exports" `; @@ -241,10 +219,8 @@ return ____exports" exports[`Bitop [5.2] ("a|=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit32.bor(a, b) - return a -end)() +a = bit32.bor(a, b) +____exports.__result = a return ____exports" `; @@ -262,10 +238,8 @@ return ____exports" exports[`Bitop [5.3] ("a&=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a & b - return a -end)() +a = a & b +____exports.__result = a return ____exports" `; @@ -277,10 +251,8 @@ return ____exports" exports[`Bitop [5.3] ("a<<=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a << b - return a -end)() +a = a << b +____exports.__result = a return ____exports" `; @@ -292,10 +264,8 @@ return ____exports" exports[`Bitop [5.3] ("a>>>=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a >> b - return a -end)() +a = a >> b +____exports.__result = a return ____exports" `; @@ -307,10 +277,8 @@ return ____exports" exports[`Bitop [5.3] ("a^=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a ~ b - return a -end)() +a = a ~ b +____exports.__result = a return ____exports" `; @@ -322,10 +290,8 @@ return ____exports" exports[`Bitop [5.3] ("a|=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a | b - return a -end)() +a = a | b +____exports.__result = a return ____exports" `; @@ -343,10 +309,8 @@ return ____exports" exports[`Bitop [5.4] ("a&=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a & b - return a -end)() +a = a & b +____exports.__result = a return ____exports" `; @@ -358,10 +322,8 @@ return ____exports" exports[`Bitop [5.4] ("a<<=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a << b - return a -end)() +a = a << b +____exports.__result = a return ____exports" `; @@ -373,10 +335,8 @@ return ____exports" exports[`Bitop [5.4] ("a>>>=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a >> b - return a -end)() +a = a >> b +____exports.__result = a return ____exports" `; @@ -388,10 +348,8 @@ return ____exports" exports[`Bitop [5.4] ("a^=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a ~ b - return a -end)() +a = a ~ b +____exports.__result = a return ____exports" `; @@ -403,10 +361,8 @@ return ____exports" exports[`Bitop [5.4] ("a|=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a | b - return a -end)() +a = a | b +____exports.__result = a return ____exports" `; @@ -424,10 +380,8 @@ return ____exports" exports[`Bitop [JIT] ("a&=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.band(a, b) - return a -end)() +a = bit.band(a, b) +____exports.__result = a return ____exports" `; @@ -439,10 +393,8 @@ return ____exports" exports[`Bitop [JIT] ("a<<=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.lshift(a, b) - return a -end)() +a = bit.lshift(a, b) +____exports.__result = a return ____exports" `; @@ -454,19 +406,15 @@ return ____exports" exports[`Bitop [JIT] ("a>>=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.arshift(a, b) - return a -end)() +a = bit.arshift(a, b) +____exports.__result = a return ____exports" `; exports[`Bitop [JIT] ("a>>>=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.rshift(a, b) - return a -end)() +a = bit.rshift(a, b) +____exports.__result = a return ____exports" `; @@ -484,10 +432,8 @@ return ____exports" exports[`Bitop [JIT] ("a^=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.bxor(a, b) - return a -end)() +a = bit.bxor(a, b) +____exports.__result = a return ____exports" `; @@ -499,10 +445,8 @@ return ____exports" exports[`Bitop [JIT] ("a|=b") 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = bit.bor(a, b) - return a -end)() +a = bit.bor(a, b) +____exports.__result = a return ____exports" `; @@ -618,10 +562,8 @@ return ____exports" exports[`Unsupported bitop 5.3 ("a>>=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a >> b - return a -end)() +a = a >> b +____exports.__result = a return ____exports" `; @@ -637,10 +579,8 @@ exports[`Unsupported bitop 5.3 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): erro exports[`Unsupported bitop 5.4 ("a>>=b"): code 1`] = ` "local ____exports = {} -____exports.__result = (function() - a = a >> b - return a -end)() +a = a >> b +____exports.__result = a return ____exports" `; diff --git a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap index 6dacea3ad..a962a4a30 100644 --- a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap +++ b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap @@ -65,11 +65,11 @@ function ____exports.__main(self) local arr = {\\"a\\", \\"b\\", \\"c\\"} local function luaIter(self) local i = 0 - return function() return arr[(function() - local ____tmp = i - i = ____tmp + 1 - return ____tmp - end)() + 1] end + return function() + local ____i_0 = i + i = ____i_0 + 1 + return arr[____i_0 + 1] + end end local result = \\"\\" return result diff --git a/test/unit/classes/__snapshots__/classes.spec.ts.snap b/test/unit/classes/__snapshots__/classes.spec.ts.snap index a900582b5..6336a53a3 100644 --- a/test/unit/classes/__snapshots__/classes.spec.ts.snap +++ b/test/unit/classes/__snapshots__/classes.spec.ts.snap @@ -2,9 +2,9 @@ exports[`missing declaration name: code 1`] = ` "require(\\"lualib_bundle\\"); -____ = __TS__Class() -____.name = \\"\\" -function ____.prototype.____constructor(self) +____class_0 = __TS__Class() +____class_0.name = \\"\\" +function ____class_0.prototype.____constructor(self) end" `; diff --git a/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap b/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap index 1f76110a4..d866c5e31 100644 --- a/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap @@ -7,11 +7,9 @@ function ____exports.__main(self) local strsArray = {{\\"a1\\", \\"a2\\"}, {\\"b1\\", \\"b2\\"}, {\\"c1\\", \\"c2\\"}} local i = 0 return function() - local strs = strsArray[(function() - local ____tmp = i - i = ____tmp + 1 - return ____tmp - end)() + 1] + local ____i_0 = i + i = ____i_0 + 1 + local strs = strsArray[____i_0 + 1] if strs then return table.unpack(strs) end @@ -32,11 +30,9 @@ function ____exports.__main(self) local strsArray = {{\\"a1\\", \\"a2\\"}, {\\"b1\\", \\"b2\\"}, {\\"c1\\", \\"c2\\"}} local i = 0 return function() - local strs = strsArray[(function() - local ____tmp = i - i = ____tmp + 1 - return ____tmp - end)() + 1] + local ____i_0 = i + i = ____i_0 + 1 + local strs = strsArray[____i_0 + 1] if strs then return table.unpack(strs) end diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index 36b928202..155203477 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -237,14 +237,12 @@ local function multi(self, ...) return ... end local a -if (function() - local ____ = { - ____(nil, 1) - } - a = ____[1] - ____exports.a = a - return ____ -end)() then +local ____temp_0 = { + ____(nil, 1) +} +a = ____temp_0[1] +____exports.a = a +if ____temp_0 then a = a + 1 ____exports.a = a end diff --git a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap index 5deed95c3..76908a6e2 100644 --- a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap @@ -47,87 +47,57 @@ return ____exports" exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("unknown"): diagnostics 1`] = `"main.ts(1,38): error TS2344: Type 'unknown' does not satisfy the constraint 'AnyNotNil'."`; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = [tableDelete({}, \\"foo\\")];"): code 1`] = ` -"foo = { - (function() - ({}).foo = nil - return nil - end)() -}" +"({}).foo = nil +foo = {nil}" `; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = [tableDelete({}, \\"foo\\")];"): diagnostics 1`] = `"main.ts(3,26): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = \`\${tableDelete({}, \\"foo\\")}\`;"): code 1`] = ` -"foo = tostring( - (function() - ({}).foo = nil - return nil - end)() -)" +"({}).foo = nil +foo = tostring(nil)" `; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = \`\${tableDelete({}, \\"foo\\")}\`;"): diagnostics 1`] = `"main.ts(3,28): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = tableDelete({}, \\"foo\\");"): code 1`] = ` -"foo = (function() - ({}).foo = nil - return nil -end)()" +"({}).foo = nil +foo = nil" `; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = tableDelete({}, \\"foo\\");"): diagnostics 1`] = `"main.ts(3,25): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("declare function foo(arg: any): void; foo(tableDelete({}, \\"foo\\"));"): code 1`] = ` -"foo( - _G, - (function() - ({}).foo = nil - return nil - end)() -)" +"({}).foo = nil +foo(_G, nil)" `; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("declare function foo(arg: any): void; foo(tableDelete({}, \\"foo\\"));"): diagnostics 1`] = `"main.ts(3,55): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = [setTable({}, \\"foo\\", 3)];"): code 1`] = ` -"foo = { - (function() - ({}).foo = 3 - return nil - end)() -}" +"({}).foo = 3 +foo = {nil}" `; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = [setTable({}, \\"foo\\", 3)];"): diagnostics 1`] = `"main.ts(3,26): error TSTL: Table set extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = \`\${setTable({}, \\"foo\\", 3)}\`;"): code 1`] = ` -"foo = tostring( - (function() - ({}).foo = 3 - return nil - end)() -)" +"({}).foo = 3 +foo = tostring(nil)" `; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = \`\${setTable({}, \\"foo\\", 3)}\`;"): diagnostics 1`] = `"main.ts(3,28): error TSTL: Table set extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = setTable({}, \\"foo\\", 3);"): code 1`] = ` -"foo = (function() - ({}).foo = 3 - return nil -end)()" +"({}).foo = 3 +foo = nil" `; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = setTable({}, \\"foo\\", 3);"): diagnostics 1`] = `"main.ts(3,25): error TSTL: Table set extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("declare function foo(arg: any): void; foo(setTable({}, \\"foo\\", 3));"): code 1`] = ` -"foo( - _G, - (function() - ({}).foo = 3 - return nil - end)() -)" +"({}).foo = 3 +foo(_G, nil)" `; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("declare function foo(arg: any): void; foo(setTable({}, \\"foo\\", 3));"): diagnostics 1`] = `"main.ts(3,55): error TSTL: Table set extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; From 0bbf30eb0a9fce0f5885f18101f94788876732d4 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 11:45:23 -0600 Subject: [PATCH 12/51] cleanup, fixes, and added some original nodes for source maps --- src/transformation/context/context.ts | 11 +++--- .../visitors/binary-expression/compound.ts | 4 +-- .../visitors/binary-expression/index.ts | 10 +++--- src/transformation/visitors/call.ts | 5 +-- src/transformation/visitors/class/index.ts | 2 +- src/transformation/visitors/conditional.ts | 15 +++++--- .../visitors/expression-list.ts | 4 ++- src/transformation/visitors/literal.ts | 36 +++++++++---------- src/transformation/visitors/loops/do-while.ts | 16 +++++++-- src/transformation/visitors/loops/for.ts | 9 +++-- src/transformation/visitors/loops/utils.ts | 4 ++- src/transformation/visitors/spread.ts | 13 ------- 12 files changed, 72 insertions(+), 57 deletions(-) diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index 8c0df27f7..b5ac763a6 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -116,13 +116,16 @@ export class TransformationContext { } public superTransformStatements(node: StatementLikeNode | readonly StatementLikeNode[]): lua.Statement[] { - return castArray(node).flatMap(n => this.superTransformNode(n) as lua.Statement[]); + return castArray(node).flatMap(n => { + this.pushPrecedingStatements(); + const statements = this.superTransformNode(n) as lua.Statement[]; + statements.unshift(...this.popPrecedingStatements()); + return statements; + }); } public pushPrecedingStatements() { - const precedingStatements: lua.Statement[] = []; - this.precedingStatementsStack.push(precedingStatements); - return precedingStatements; + this.precedingStatementsStack.push([]); } public popPrecedingStatements() { diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index 2186ba76c..d958524c3 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -267,7 +267,7 @@ function transformSetterSkippingCompoundAssignment( condition = lhs; } else if (operator === ts.SyntaxKind.BarBarToken) { condition = lua.createUnaryExpression(lhs, lua.SyntaxKind.NotOperator); - } else if (isSetterSkippingCompoundAssignmentOperator(operator)) { + } else if (operator === ts.SyntaxKind.QuestionQuestionToken) { condition = lua.createBinaryExpression(lhs, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator); } else { assertNever(operator); @@ -277,7 +277,7 @@ function transformSetterSkippingCompoundAssignment( return [ lua.createIfStatement( condition, - lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(lhs, right)]), + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(lhs, right, node)]), undefined, node ), diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 1d2ba5e4a..fd8cf3216 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -87,10 +87,12 @@ function createShortCircuitBinaryExpression( const rightPrecedingStatements = context.popPrecedingStatements(); if (rightPrecedingStatements.length > 0) { const result = context.createTempForLuaExpression(lhs); - const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs); + const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs, node.left); const ifStatement = lua.createIfStatement( createCondition(lua.cloneIdentifier(result)), - lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(result, rhs)]) + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(result, rhs)]), + undefined, + node.left ); context.addPrecedingStatements([assignmentStatement, ifStatement]); return result; @@ -151,13 +153,13 @@ export const transformBinaryExpression: FunctionVisitor = ( case ts.SyntaxKind.QuestionQuestionToken: { return createShortCircuitBinaryExpression(context, node, operator, i => - lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator) + lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator, node) ); } case ts.SyntaxKind.BarBarToken: { return createShortCircuitBinaryExpression(context, node, operator, i => - lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator) + lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator, node) ); } diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 456f3fea1..a7c6a56cd 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -10,10 +10,7 @@ import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { isValidLuaIdentifier } from "../utils/safe-names"; import { isExpressionWithEvaluationEffect } from "../utils/typescript"; import { transformElementAccessArgument } from "./access"; -import { - isMultiReturnCall, - /* isMultiReturnType, */ shouldMultiReturnCallBeWrapped, -} from "./language-extensions/multi"; +import { isMultiReturnCall, shouldMultiReturnCallBeWrapped } from "./language-extensions/multi"; import { isOperatorMapping, transformOperatorMappingExpression } from "./language-extensions/operators"; import { isTableDeleteCall, diff --git a/src/transformation/visitors/class/index.ts b/src/transformation/visitors/class/index.ts index d0c8afede..03bc661f3 100644 --- a/src/transformation/visitors/class/index.ts +++ b/src/transformation/visitors/class/index.ts @@ -66,7 +66,7 @@ function transformClassLikeDeclaration( } else if (classDeclaration.name !== undefined) { className = transformIdentifier(context, classDeclaration.name); } else { - className = lua.createIdentifier(context.createTempName("class")); + className = lua.createIdentifier(context.createTempName("class"), classDeclaration); } const annotations = getTypeAnnotations(context.checker.getTypeAtLocation(classDeclaration)); diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts index f44e9acfc..bf16d1c36 100644 --- a/src/transformation/visitors/conditional.ts +++ b/src/transformation/visitors/conditional.ts @@ -31,23 +31,28 @@ function transformProtectedConditionalExpression( context: TransformationContext, expression: ts.ConditionalExpression ): lua.Expression { - const tempVar = lua.createIdentifier(context.createTempName("temp")); + const tempVar = context.createTempForExpression(expression.condition); const condition = context.transformExpression(expression.condition); context.pushPrecedingStatements(); const val1 = context.transformExpression(expression.whenTrue); const trueStatements = context.popPrecedingStatements(); - trueStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val1)); + trueStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val1, expression.whenTrue)); context.pushPrecedingStatements(); const val2 = context.transformExpression(expression.whenFalse); const falseStatements = context.popPrecedingStatements(); - falseStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val2)); + falseStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val2, expression.whenFalse)); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar)]); + context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, undefined, expression.condition)]); context.addPrecedingStatements([ - lua.createIfStatement(condition, lua.createBlock(trueStatements), lua.createBlock(falseStatements), expression), + lua.createIfStatement( + condition, + lua.createBlock(trueStatements, expression.whenTrue), + lua.createBlock(falseStatements, expression.whenFalse), + expression + ), ]); return lua.cloneIdentifier(tempVar); } diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 9d4ad7eeb..fcb487532 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -53,7 +53,9 @@ function processPrecedingStatements( // Inject temp assignment in correct place in preceding statements const tempVar = context.createTempForLuaExpression(info.transformedExpression); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, expression)]); + const tempDeclaration = lua.createVariableDeclarationStatement(tempVar, expression); + lua.setNodePosition(tempDeclaration, lua.getOriginalPos(expression)); + context.addPrecedingStatements([tempDeclaration]); info.transformedExpression = lua.cloneIdentifier(tempVar); } } diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 4c9c5c31d..80e24c100 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -71,7 +71,7 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor= 0) { - for (let i = 0; i < transformedProperties.length; ++i) { - const property = transformedProperties[i]; + for (let i = 0; i < properties.length; ++i) { + const property = properties[i]; const propertyPrecedingStatements = precedingStatements[i]; context.addPrecedingStatements(propertyPrecedingStatements); @@ -163,31 +163,31 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor 0) { - tableExpressions.push(lua.createTableExpression(properties)); + if (fields.length > 0) { + tableExpressions.push(lua.createTableExpression(fields)); } tableExpressions.push(property); - properties = []; + fields = []; } } if (tableExpressions.length === 0) { - return lua.createTableExpression(properties, expression); + return lua.createTableExpression(fields, expression); } else { - if (properties.length > 0) { - const tableExpression = lua.createTableExpression(properties, expression); + if (fields.length > 0) { + const tableExpression = lua.createTableExpression(fields, expression); tableExpressions.push(tableExpression); } diff --git a/src/transformation/visitors/loops/do-while.ts b/src/transformation/visitors/loops/do-while.ts index 205fc5252..73148605e 100644 --- a/src/transformation/visitors/loops/do-while.ts +++ b/src/transformation/visitors/loops/do-while.ts @@ -13,7 +13,12 @@ export const transformWhileStatement: FunctionVisitor = (stat // Change from 'while condition' to 'while true - if not condition then break' if (precedingStatements.length > 0) { precedingStatements.push( - lua.createIfStatement(invertCondition(condition), lua.createBlock([lua.createBreakStatement()])) + lua.createIfStatement( + invertCondition(condition), + lua.createBlock([lua.createBreakStatement()]), + undefined, + statement.expression + ) ); body.unshift(...precedingStatements); condition = lua.createBooleanLiteral(true); @@ -31,7 +36,14 @@ export const transformDoStatement: FunctionVisitor = (statement, // Change from 'repeat until not condition' to 'repeat - if not condition break - until false' if (precedingStatements.length > 0) { - precedingStatements.push(lua.createIfStatement(condition, lua.createBlock([lua.createBreakStatement()]))); + precedingStatements.push( + lua.createIfStatement( + condition, + lua.createBlock([lua.createBreakStatement()]), + undefined, + statement.expression + ) + ); condition = lua.createBooleanLiteral(false); } diff --git a/src/transformation/visitors/loops/for.ts b/src/transformation/visitors/loops/for.ts index d5b267b86..cf1665936 100644 --- a/src/transformation/visitors/loops/for.ts +++ b/src/transformation/visitors/loops/for.ts @@ -28,7 +28,12 @@ export const transformForStatement: FunctionVisitor = (statemen // Change 'while condition' to 'while true - if not condition break' if (precedingStatements.length > 0) { precedingStatements.push( - lua.createIfStatement(invertCondition(condition), lua.createBlock([lua.createBreakStatement()])) + lua.createIfStatement( + invertCondition(condition), + lua.createBlock([lua.createBreakStatement()]), + undefined, + statement.condition + ) ); body.unshift(...precedingStatements); condition = lua.createBooleanLiteral(true); @@ -42,7 +47,7 @@ export const transformForStatement: FunctionVisitor = (statemen } // while (condition) do ... end - result.push(lua.createWhileStatement(lua.createBlock(body), condition)); + result.push(lua.createWhileStatement(lua.createBlock(body), condition, statement)); return lua.createDoStatement(result, statement); }; diff --git a/src/transformation/visitors/loops/utils.ts b/src/transformation/visitors/loops/utils.ts index cc4d8638d..c51a262be 100644 --- a/src/transformation/visitors/loops/utils.ts +++ b/src/transformation/visitors/loops/utils.ts @@ -76,6 +76,8 @@ export function invertCondition(expression: lua.Expression) { if (lua.isUnaryExpression(expression) && expression.operator === lua.SyntaxKind.NotOperator) { return expression.operand; } else { - return lua.createUnaryExpression(expression, lua.SyntaxKind.NotOperator); + const notExpression = lua.createUnaryExpression(expression, lua.SyntaxKind.NotOperator); + lua.setNodePosition(notExpression, lua.getOriginalPos(expression)); + return notExpression; } } diff --git a/src/transformation/visitors/spread.ts b/src/transformation/visitors/spread.ts index 9132c1663..f8ebdc79d 100644 --- a/src/transformation/visitors/spread.ts +++ b/src/transformation/visitors/spread.ts @@ -61,19 +61,6 @@ export function isOptimizedVarArgSpread(context: TransformationContext, symbol: return true; } -export function isOptimizedVarArgSpreadElement(context: TransformationContext, spreadElement: ts.SpreadElement) { - if (!ts.isIdentifier(spreadElement.expression)) { - return false; - } - - const symbol = context.checker.getSymbolAtLocation(spreadElement.expression); - if (!symbol || !isOptimizedVarArgSpread(context, symbol, spreadElement.expression)) { - return false; - } - - return true; -} - // TODO: Currently it's also used as an array member export const transformSpreadElement: FunctionVisitor = (node, context) => { if (ts.isIdentifier(node.expression)) { From c78843242dca7c3cbbdac0d2fcc76f75bea4be6a Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 12:54:52 -0600 Subject: [PATCH 13/51] comment update --- src/transformation/visitors/expression-list.ts | 2 +- src/transformation/visitors/literal.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index fcb487532..0f4730343 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -43,7 +43,7 @@ function processPrecedingStatements( continue; } - // Strip 'unpack' from spreads - it will be added back later in buildArrayConcatCall + // Strip 'unpack' from spreads to store in a temp - it will be added back in buildArrayConcatCall, if needed let expression = info.transformedExpression; if (info.isSpread) { assert(lua.isCallExpression(expression) && expression.params.length === 1); diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 80e24c100..258175f9e 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -144,12 +144,17 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor= lastPrecedingStatementsIndex) continue; if (lua.isTableFieldExpression(property)) { + // Skip fields whose values are: + // - literal values that couldn't be affected by preceding statements + // - temp identifiers which are results from preceding statements if ( !lua.isLiteral(property.value) && !(propertyPrecedingStatements.length > 0 && lua.isIdentifier(property.value)) From 2b3f517f7736c0af89c145318757a18ba5d35846 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 13:37:36 -0600 Subject: [PATCH 14/51] fixed ifelse statements --- src/transformation/visitors/conditional.ts | 9 ++++++++- test/unit/precedingStatements.spec.ts | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts index bf16d1c36..e5f7a711b 100644 --- a/src/transformation/visitors/conditional.ts +++ b/src/transformation/visitors/conditional.ts @@ -80,8 +80,15 @@ export function transformIfStatement(statement: ts.IfStatement, context: Transfo if (statement.elseStatement) { if (ts.isIfStatement(statement.elseStatement)) { + context.pushPrecedingStatements(); const elseStatement = transformIfStatement(statement.elseStatement, context); - return lua.createIfStatement(condition, ifBlock, elseStatement); + const precedingStatements = context.popPrecedingStatements(); + if (precedingStatements.length > 0) { + const elseBlock = lua.createBlock([...precedingStatements, elseStatement]); + return lua.createIfStatement(condition, ifBlock, elseBlock); + } else { + return lua.createIfStatement(condition, ifBlock, elseStatement); + } } else { pushScope(context, ScopeType.Conditional); const elseStatements = performHoisting( diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 88bda7014..9757cbf45 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -165,3 +165,13 @@ test("switch scoping", () => { } `.expectToMatchJsResult(); }); + +test("else if", () => { + util.testFunction` + let i = 0; + if (i++ === 0) { + } else if (i++ === 1) { + } + return i; + `.expectToMatchJsResult(); +}); From 00d6bdbe5437aa7374c499cdce9c0062a4fa8d2e Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 13:52:41 -0600 Subject: [PATCH 15/51] more things to fix --- test/unit/precedingStatements.spec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 9757cbf45..0c959f7a0 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -175,3 +175,30 @@ test("else if", () => { return i; `.expectToMatchJsResult(); }); + +test("template expression", () => { + util.testFunction` + let i = 0; + return \`\${i} - \${i++}\` + `.expectToMatchJsResult(); +}); + +test("tagged template literal", () => { + util.testFunction` + function func(strings: TemplateStringsArray, ...expressions: any[]) { + return { strings: [...strings], raw: strings.raw, expressions }; + } + + let i = 0; + return func\`hello \${i} \${i++}\`; + `.expectToMatchJsResult(); +}); + +test("compound access", () => { + util.testFunction` + let i = 0; + const a = [1, 2, 3]; + a[i] = i++; + return a[0]; + `.expectToMatchJsResult(); +}); From e7ba530c881b7cde2dde0ccd094700d9db348eaa Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Sep 2021 18:58:03 -0600 Subject: [PATCH 16/51] working on fixes to assignments and creating more tests (most of which are broken) --- src/transformation/context/context.ts | 8 +- .../visitors/binary-expression/assignments.ts | 70 ++++++++------- .../visitors/binary-expression/compound.ts | 4 +- .../visitors/binary-expression/index.ts | 23 +++-- src/transformation/visitors/conditional.ts | 2 +- test/unit/precedingStatements.spec.ts | 88 +++++++++++++------ 6 files changed, 123 insertions(+), 72 deletions(-) diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index b5ac763a6..d7a6fa8ae 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -162,14 +162,14 @@ export class TransformationContext { return identifier; } - public createTempForExpression(expression: ts.Expression) { + public createTempForNode(node: ts.Node) { let name: string | undefined; - if (ts.isStringLiteral(expression) || ts.isIdentifier(expression)) { - name = expression.text; + if (ts.isStringLiteral(node) || ts.isIdentifier(node) || ts.isMemberName(node)) { + name = node.text; if (!isValidLuaIdentifier(name)) { name = fixInvalidLuaIdentifier(name); } } - return lua.createIdentifier(this.createTempName(name), expression); + return lua.createIdentifier(this.createTempName(name), node); } } diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index 64a71f890..2a9bd58f1 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -1,17 +1,16 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; -import { cast } from "../../../utils"; +import { assert, cast } from "../../../utils"; import { TransformationContext } from "../../context"; import { validateAssignment } from "../../utils/assignment-validation"; import { createExportedIdentifier, getDependenciesOfSymbol, isSymbolExported } from "../../utils/export"; import { createUnpackCall, wrapInTable } from "../../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; import { isArrayType, isDestructuringAssignment } from "../../utils/typescript"; -import { transformElementAccessArgument } from "../access"; import { isArrayLength, transformDestructuringAssignment } from "./destructuring-assignments"; import { isMultiReturnCall } from "../language-extensions/multi"; -import { popScope, pushScope, ScopeType } from "../../utils/scope"; import { notAllowedOptionalAssignment } from "../../utils/diagnostics"; +import { transformElementAccessArgument } from "../access"; export function transformAssignmentLeftHandSideExpression( context: TransformationContext, @@ -75,7 +74,7 @@ function transformDestructuredAssignmentExpression( context: TransformationContext, expression: ts.DestructuringAssignment ) { - const rootIdentifier = context.createTempForExpression(expression.right); + const rootIdentifier = context.createTempForNode(expression.right); let right = context.transformExpression(expression.right); if (isMultiReturnCall(context, expression.right)) { @@ -117,35 +116,44 @@ export function transformAssignmentExpression( } if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) { - // Left is property/element access: cache result while maintaining order of evaluation - // (function(o, i, v) o[i] = v; return v end)(${objExpression}, ${indexExpression}, ${right}) - const objParameter = lua.createIdentifier("o"); - const indexParameter = lua.createIdentifier("i"); - const valueParameter = lua.createIdentifier("v"); - const indexStatement = lua.createTableIndexExpression(objParameter, indexParameter); - const statements: lua.Statement[] = [ - lua.createAssignmentStatement(indexStatement, valueParameter), - lua.createReturnStatement([valueParameter]), - ]; - const iife = lua.createFunctionExpression(lua.createBlock(statements), [ - objParameter, - indexParameter, - valueParameter, - ]); - pushScope(context, ScopeType.Function); - const objExpression = context.transformExpression(expression.left.expression); - let indexExpression: lua.Expression; - if (ts.isPropertyAccessExpression(expression.left)) { - // Property access - indexExpression = lua.createStringLiteral(expression.left.name.text); - } else { - // Element access - indexExpression = transformElementAccessArgument(context, expression.left); + const tempVar = context.createTempForNode(expression.right); + context.pushPrecedingStatements(); + const right = context.transformExpression(expression.right); + const precedingStatements = context.popPrecedingStatements(); + + let left: lua.Expression | undefined; + if (precedingStatements.length > 0) { + let indexNode: ts.Node; + let index: lua.Expression; + if (ts.isElementAccessExpression(expression.left)) { + indexNode = expression.left.argumentExpression; + index = transformElementAccessArgument(context, expression.left); + } else { + indexNode = expression.left.name; + index = lua.createStringLiteral(expression.left.name.text); + } + if (!lua.isLiteral(index)) { + const indexTemp = context.createTempForNode(indexNode); + context.addPrecedingStatements([lua.createVariableDeclarationStatement(indexTemp, index, indexNode)]); + left = lua.createTableIndexExpression( + context.transformExpression(expression.left.expression), + lua.cloneIdentifier(indexTemp), + expression.left + ); + } + context.addPrecedingStatements(precedingStatements); } - const args = [objExpression, indexExpression, context.transformExpression(expression.right)]; - popScope(context); - return lua.createCallExpression(iife, args, expression); + if (!left) { + left = context.transformExpression(expression.left); + } + assert(lua.isAssignmentLeftHandSideExpression(left)); + + context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(tempVar, right, expression.right), + lua.createAssignmentStatement(left, lua.cloneIdentifier(tempVar, expression.left)), + ]); + return lua.cloneIdentifier(tempVar); } else { // Simple assignment // ${left} = ${right}; return ${left} diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index d958524c3..5305aa254 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -91,8 +91,8 @@ export function transformCompoundAssignment( if (objExpression && indexExpression) { // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects // local __obj, __index = ${objExpression}, ${indexExpression}; - const obj = context.createTempForExpression(objExpression); - const index = context.createTempForExpression(indexExpression); + const obj = context.createTempForNode(objExpression); + const index = context.createTempForNode(indexExpression); const objAndIndexDeclaration = lua.createVariableDeclarationStatement( [obj, index], [context.transformExpression(objExpression), context.transformExpression(indexExpression)] diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index fd8cf3216..0af1273bd 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -168,13 +168,22 @@ export const transformBinaryExpression: FunctionVisitor = ( } } - return transformBinaryOperation( - context, - context.transformExpression(node.left), - context.transformExpression(node.right), - operator, - node - ); + const lhs = context.transformExpression(node.left); + context.pushPrecedingStatements(); + const rhs = context.transformExpression(node.right); + const precedingStatements = context.popPrecedingStatements(); + + // Cache left in temp if right had preceding statements that may have referenced things in the left + if (precedingStatements.length > 0) { + const tempVar = context.createTempForNode(node.left); + context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(tempVar, lhs, node.left), + ...precedingStatements, + ]); + return transformBinaryOperation(context, tempVar, rhs, operator, node); + } + + return transformBinaryOperation(context, lhs, rhs, operator, node); }; export function transformBinaryExpressionStatement( diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts index e5f7a711b..57b8780ab 100644 --- a/src/transformation/visitors/conditional.ts +++ b/src/transformation/visitors/conditional.ts @@ -31,7 +31,7 @@ function transformProtectedConditionalExpression( context: TransformationContext, expression: ts.ConditionalExpression ): lua.Expression { - const tempVar = context.createTempForExpression(expression.condition); + const tempVar = context.createTempForNode(expression.condition); const condition = context.transformExpression(expression.condition); diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 0c959f7a0..b3851eb23 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -104,6 +104,67 @@ describe("execution order", () => { return [a, b, c, d]; `.expectToMatchJsResult(); }); + + test("template expression", () => { + util.testFunction` + let i = 0; + return \`\${i} - \${i++}\` + `.expectToMatchJsResult(); + }); + + test("tagged template literal", () => { + util.testFunction` + function func(strings: TemplateStringsArray, ...expressions: any[]) { + return { strings: [...strings], raw: strings.raw, expressions }; + } + + let i = 0; + return func\`hello \${i} \${i++}\`; + `.expectToMatchJsResult(); + }); + + test("binary operators", () => { + util.testFunction` + let i = 0; + return i + i++; + `.expectToMatchJsResult(); + }); + + test("index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8, 7]; + a[i] = i++; + return a; + `.expectToMatchJsResult(); + }); + + test("index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8, 7]; + const x = a[i] = i++; + return a; + `.expectToMatchJsResult(); + }); + + test("destructuring assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8, 7]; + [a[i++], a[i]] = [i++, i]; + return a; + `.expectToMatchJsResult(); + }); + + test("destructuring assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8, 7]; + const x = [a[i++], a[i]] = [i++, i]; + return a; + `.expectToMatchJsResult(); + }); }); describe("loop expressions", () => { @@ -175,30 +236,3 @@ test("else if", () => { return i; `.expectToMatchJsResult(); }); - -test("template expression", () => { - util.testFunction` - let i = 0; - return \`\${i} - \${i++}\` - `.expectToMatchJsResult(); -}); - -test("tagged template literal", () => { - util.testFunction` - function func(strings: TemplateStringsArray, ...expressions: any[]) { - return { strings: [...strings], raw: strings.raw, expressions }; - } - - let i = 0; - return func\`hello \${i} \${i++}\`; - `.expectToMatchJsResult(); -}); - -test("compound access", () => { - util.testFunction` - let i = 0; - const a = [1, 2, 3]; - a[i] = i++; - return a[0]; - `.expectToMatchJsResult(); -}); From d60b6fd340003c9ea5cc3fb7aefc9762419e515d Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 4 Sep 2021 10:15:06 -0600 Subject: [PATCH 17/51] more fixes. more broken things. --- .../visitors/binary-expression/assignments.ts | 72 +++++++++------- .../visitors/binary-expression/compound.ts | 86 +++++++------------ test/unit/precedingStatements.spec.ts | 78 +++++++++++++++++ 3 files changed, 153 insertions(+), 83 deletions(-) diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index 2a9bd58f1..19f1e9b04 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -1,6 +1,6 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; -import { assert, cast } from "../../../utils"; +import { cast } from "../../../utils"; import { TransformationContext } from "../../context"; import { validateAssignment } from "../../utils/assignment-validation"; import { createExportedIdentifier, getDependenciesOfSymbol, isSymbolExported } from "../../utils/export"; @@ -24,11 +24,38 @@ export function transformAssignmentLeftHandSideExpression( : cast(left, lua.isAssignmentLeftHandSideExpression); } +function transformAssignmentLeftHandSideExpressionWithRightPrecedingStatements( + context: TransformationContext, + expression: ts.Expression +) { + // Cache index expression in a temp so it can be evaluated before right's preceding statements + if (ts.isElementAccessExpression(expression) && !ts.isLiteralExpression(expression.argumentExpression)) { + let table = context.transformExpression(expression.expression); + + // If table is complex, it could reference things from the index expression and needs to be cached as well + if (!ts.isIdentifier(expression.expression)) { + const tableTemp = context.createTempForNode(expression.expression); + context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(tableTemp, table, expression.expression), + ]); + table = lua.cloneIdentifier(tableTemp); + } + + const indexNode = expression.argumentExpression; + const indexTemp = context.createTempForNode(indexNode); + const index = transformElementAccessArgument(context, expression); + context.addPrecedingStatements([lua.createVariableDeclarationStatement(indexTemp, index, indexNode)]); + return lua.createTableIndexExpression(table, lua.cloneIdentifier(indexTemp), expression); + } + return transformAssignmentLeftHandSideExpression(context, expression); +} + export function transformAssignment( context: TransformationContext, // TODO: Change type to ts.LeftHandSideExpression? lhs: ts.Expression, right: lua.Expression, + rightPrecedingStatements?: lua.Statement[], parent?: ts.Expression ): lua.Statement[] { if (ts.isOptionalChain(lhs)) { @@ -56,11 +83,15 @@ export function transformAssignment( const dependentSymbols = symbol ? getDependenciesOfSymbol(context, symbol) : []; - const left = transformAssignmentLeftHandSideExpression(context, lhs); + const left = + rightPrecedingStatements && rightPrecedingStatements.length > 0 + ? transformAssignmentLeftHandSideExpressionWithRightPrecedingStatements(context, lhs) + : transformAssignmentLeftHandSideExpression(context, lhs); const rootAssignment = lua.createAssignmentStatement(left, right, lhs.parent); return [ + ...(rightPrecedingStatements ?? []), rootAssignment, ...dependentSymbols.map(symbol => { const [left] = rootAssignment.left; @@ -121,35 +152,13 @@ export function transformAssignmentExpression( const right = context.transformExpression(expression.right); const precedingStatements = context.popPrecedingStatements(); - let left: lua.Expression | undefined; - if (precedingStatements.length > 0) { - let indexNode: ts.Node; - let index: lua.Expression; - if (ts.isElementAccessExpression(expression.left)) { - indexNode = expression.left.argumentExpression; - index = transformElementAccessArgument(context, expression.left); - } else { - indexNode = expression.left.name; - index = lua.createStringLiteral(expression.left.name.text); - } - if (!lua.isLiteral(index)) { - const indexTemp = context.createTempForNode(indexNode); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(indexTemp, index, indexNode)]); - left = lua.createTableIndexExpression( - context.transformExpression(expression.left.expression), - lua.cloneIdentifier(indexTemp), - expression.left - ); - } - context.addPrecedingStatements(precedingStatements); - } - - if (!left) { - left = context.transformExpression(expression.left); - } - assert(lua.isAssignmentLeftHandSideExpression(left)); + const left = + precedingStatements.length > 0 + ? transformAssignmentLeftHandSideExpressionWithRightPrecedingStatements(context, expression.left) + : transformAssignmentLeftHandSideExpression(context, expression.left); context.addPrecedingStatements([ + ...precedingStatements, lua.createVariableDeclarationStatement(tempVar, right, expression.right), lua.createAssignmentStatement(left, lua.cloneIdentifier(tempVar, expression.left)), ]); @@ -223,6 +232,9 @@ export function transformAssignmentStatement( ...transformDestructuringAssignment(context, expression, rootIdentifier), ]; } else { - return transformAssignment(context, expression.left, context.transformExpression(expression.right)); + context.pushPrecedingStatements(); + const right = context.transformExpression(expression.right); + const precedingStatements = context.popPrecedingStatements(); + return transformAssignment(context, expression.left, right, precedingStatements); } } diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index 5305aa254..501d3b1ce 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -2,34 +2,22 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { cast, assertNever } from "../../../utils"; import { TransformationContext } from "../../context"; -import { isArrayType, isExpressionWithEvaluationEffect } from "../../utils/typescript"; import { transformBinaryOperation } from "../binary-expression"; import { transformAssignment } from "./assignments"; -// If expression is property/element access with possible effects from being evaluated, returns separated object and index expressions. -export function parseAccessExpressionWithEvaluationEffects( - context: TransformationContext, - node: ts.Expression -): [ts.Expression, ts.Expression] | [] { - if ( - ts.isElementAccessExpression(node) && - (isExpressionWithEvaluationEffect(node.expression) || isExpressionWithEvaluationEffect(node.argumentExpression)) - ) { - const type = context.checker.getTypeAtLocation(node.expression); - if (isArrayType(context, type)) { - // Offset arrays by one - const oneLit = ts.factory.createNumericLiteral("1"); - const exp = ts.factory.createParenthesizedExpression(node.argumentExpression); - const addExp = ts.factory.createBinaryExpression(exp, ts.SyntaxKind.PlusToken, oneLit); - return [node.expression, addExp]; - } else { - return [node.expression, node.argumentExpression]; - } - } else if (ts.isPropertyAccessExpression(node) && isExpressionWithEvaluationEffect(node.expression)) { - return [node.expression, ts.factory.createStringLiteral(node.name.text)]; - } +function isLuaExpressionWithSideEffect(expression: lua.Expression) { + return !(lua.isLiteral(expression) || lua.isIdentifier(expression)); +} - return []; +function shouldCacheTableIndexExpressions( + expression: lua.TableIndexExpression, + rightPrecedingStatements: lua.Statement[] +) { + return ( + isLuaExpressionWithSideEffect(expression.table) || + isLuaExpressionWithSideEffect(expression.index) || + rightPrecedingStatements.length > 0 + ); } // TODO: `as const` doesn't work on enum members @@ -87,16 +75,13 @@ export function transformCompoundAssignment( const right = context.transformExpression(rhs); const rightPrecedingStatements = context.popPrecedingStatements(); - const [objExpression, indexExpression] = parseAccessExpressionWithEvaluationEffects(context, lhs); - if (objExpression && indexExpression) { + if (lua.isTableIndexExpression(left) && shouldCacheTableIndexExpressions(left, rightPrecedingStatements)) { // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects // local __obj, __index = ${objExpression}, ${indexExpression}; - const obj = context.createTempForNode(objExpression); - const index = context.createTempForNode(indexExpression); - const objAndIndexDeclaration = lua.createVariableDeclarationStatement( - [obj, index], - [context.transformExpression(objExpression), context.transformExpression(indexExpression)] - ); + const obj = context.createTempForLuaExpression(left.table); + const index = context.createTempForLuaExpression(left.index); + + const objAndIndexDeclaration = lua.createVariableDeclarationStatement([obj, index], [left.table, left.index]); const accessExpression = lua.createTableIndexExpression(obj, index); const tmp = context.createTempForLuaExpression(left); @@ -116,8 +101,10 @@ export function transformCompoundAssignment( assignStatement = lua.createAssignmentStatement(accessExpression, tmp); } // return ____tmp - context.addPrecedingStatements(rightPrecedingStatements); - return { statements: [objAndIndexDeclaration, tmpDeclaration, assignStatement], result: tmp }; + return { + statements: [objAndIndexDeclaration, ...rightPrecedingStatements, tmpDeclaration, assignStatement], + result: tmp, + }; } else if (isPostfix) { // Postfix expressions need to cache original value in temp // local ____tmp = ${left}; @@ -126,8 +113,7 @@ export function transformCompoundAssignment( const tmpIdentifier = context.createTempForLuaExpression(left); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); const operatorExpression = transformBinaryOperation(context, tmpIdentifier, right, operator, expression); - const assignStatements = transformAssignment(context, lhs, operatorExpression); - context.addPrecedingStatements(rightPrecedingStatements); + const assignStatements = transformAssignment(context, lhs, operatorExpression, rightPrecedingStatements); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else if (ts.isPropertyAccessExpression(lhs) || ts.isElementAccessExpression(lhs)) { // Simple property/element access expressions need to cache in temp to avoid double-evaluation @@ -137,7 +123,6 @@ export function transformCompoundAssignment( const tmpIdentifier = context.createTempForLuaExpression(left); const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, operatorExpression); - const assignStatements = transformAssignment(context, lhs, tmpIdentifier); if (isSetterSkippingCompoundAssignmentOperator(operator)) { const statements = [ @@ -147,13 +132,13 @@ export function transformCompoundAssignment( return { statements, result: tmpIdentifier }; } - context.addPrecedingStatements(rightPrecedingStatements); + const assignStatements = transformAssignment(context, lhs, tmpIdentifier, rightPrecedingStatements); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else { // Simple expressions - // ${left} = ${right}; return ${right} + // ${left} = ${left} ${operator} ${right} const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); - const statements = transformAssignment(context, lhs, operatorExpression); + const statements = transformAssignment(context, lhs, operatorExpression, rightPrecedingStatements); if (rightPrecedingStatements.length > 0 && isSetterSkippingCompoundAssignmentOperator(operator)) { return { @@ -162,7 +147,6 @@ export function transformCompoundAssignment( }; } - context.addPrecedingStatements(rightPrecedingStatements); return { statements, result: left }; } } @@ -193,17 +177,14 @@ export function transformCompoundAssignmentStatement( const right = context.transformExpression(rhs); const rightPrecedingStatements = context.popPrecedingStatements(); - const [objExpression, indexExpression] = parseAccessExpressionWithEvaluationEffects(context, lhs); - if (objExpression && indexExpression) { + if (lua.isTableIndexExpression(left) && shouldCacheTableIndexExpressions(left, rightPrecedingStatements)) { // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects // local __obj, __index = ${objExpression}, ${indexExpression}; // ____obj[____index] = ____obj[____index] ${replacementOperator} ${right}; - const obj = lua.createIdentifier("____obj"); - const index = lua.createIdentifier("____index"); - const objAndIndexDeclaration = lua.createVariableDeclarationStatement( - [obj, index], - [context.transformExpression(objExpression), context.transformExpression(indexExpression)] - ); + const obj = context.createTempForLuaExpression(left.table); + const index = context.createTempForLuaExpression(left.index); + + const objAndIndexDeclaration = lua.createVariableDeclarationStatement([obj, index], [left.table, left.index]); const accessExpression = lua.createTableIndexExpression(obj, index); if (isSetterSkippingCompoundAssignmentOperator(operator)) { @@ -221,8 +202,7 @@ export function transformCompoundAssignmentStatement( const operatorExpression = transformBinaryOperation(context, accessExpression, right, operator, node); const assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); - context.addPrecedingStatements(rightPrecedingStatements); - return [objAndIndexDeclaration, assignStatement]; + return [objAndIndexDeclaration, ...rightPrecedingStatements, assignStatement]; } else { if (isSetterSkippingCompoundAssignmentOperator(operator)) { return transformSetterSkippingCompoundAssignment(left, operator, right, rightPrecedingStatements, node); @@ -230,9 +210,9 @@ export function transformCompoundAssignmentStatement( // Simple statements // ${left} = ${left} ${replacementOperator} ${right} + const operatorExpression = transformBinaryOperation(context, left, right, operator, node); - context.addPrecedingStatements(rightPrecedingStatements); - return transformAssignment(context, lhs, operatorExpression); + return transformAssignment(context, lhs, operatorExpression, rightPrecedingStatements); } } diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index b3851eb23..bc4005b8b 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -148,6 +148,64 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); + test("indirect index assignment statement", () => { + util.testFunction` + let i = 1; + const a = [9, 8, 7]; + function foo(x: number) { i += x; return a; } + foo(i)[i] = i++; + return a; + `.expectToMatchJsResult(); + }); + + test("indirect index assignment expression", () => { + util.testFunction` + let i = 1; + const a = [9, 8, 7]; + function foo(x: number) { i += x; return a; } + const x = foo(i)[i] = i++; + return a; + `.expectToMatchJsResult(); + }); + + test("compound index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8, 7]; + a[i] += i++; + return a; + `.expectToMatchJsResult(); + }); + + test("compound index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8, 7]; + const x = a[i] += i++; + return a; + `.expectToMatchJsResult(); + }); + + test("compound indirect index assignment statement", () => { + util.testFunction` + let i = 1; + const a = [9, 8, 7]; + function foo(x: number) { i += x; return a; } + foo(i)[i] += i++; + return a; + `.expectToMatchJsResult(); + }); + + test("compound indirect index assignment expression", () => { + util.testFunction` + let i = 1; + const a = [9, 8, 7]; + function foo(x: number) { i += x; return a; } + const x = foo(i)[i] += i++; + return a; + `.expectToMatchJsResult(); + }); + test("destructuring assignment statement", () => { util.testFunction` let i = 0; @@ -165,6 +223,26 @@ describe("execution order", () => { return a; `.expectToMatchJsResult(); }); + + test("call statement", () => { + util.testFunction` + let i = 1; + const a = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + function foo(x: number) { i += x; return ((y: number) => { i += y; return a; }); } + foo(i++)(i++)[i] = 0; + return a; + `.expectToMatchJsResult(); + }); + + test("call expression", () => { + util.testFunction` + let i = 1; + const a = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + function foo(x: number) { i += x; return ((y: number) => { i += y; return a; }); } + const x = foo(i++)(i++)[i] = 0; + return a; + `.expectToMatchJsResult(); + }); }); describe("loop expressions", () => { From ed8bc68bf2bece60bcc0d7998ebe7a36de360c87 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 5 Sep 2021 10:23:04 -0600 Subject: [PATCH 18/51] lots of fixes to call expressions and lots of new broken tests --- src/transformation/visitors/access.ts | 22 +- .../visitors/binary-expression/assignments.ts | 2 +- .../visitors/binary-expression/index.ts | 17 +- src/transformation/visitors/call.ts | 109 +++++++-- .../visitors/expression-list.ts | 67 +++++- src/transformation/visitors/literal.ts | 12 +- test/unit/precedingStatements.spec.ts | 214 ++++++++++++++---- 7 files changed, 348 insertions(+), 95 deletions(-) diff --git a/src/transformation/visitors/access.ts b/src/transformation/visitors/access.ts index 389bbde5f..2c15a58b1 100644 --- a/src/transformation/visitors/access.ts +++ b/src/transformation/visitors/access.ts @@ -8,6 +8,7 @@ import { addToNumericExpression } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { isArrayType, isNumberType, isStringType } from "../utils/typescript"; import { tryGetConstEnumValue } from "./enum"; +import { transformOrderedExpressions } from "./expression-list"; import { isMultiReturnCall, returnsMultiType } from "./language-extensions/multi"; export function transformElementAccessArgument( @@ -25,22 +26,35 @@ export function transformElementAccessArgument( return index; } +export function getElementAccessArgument( + context: TransformationContext, + node: ts.ElementAccessExpression, + index: lua.Expression +): lua.Expression { + const type = context.checker.getTypeAtLocation(node.expression); + const argumentType = context.checker.getTypeAtLocation(node.argumentExpression); + if (isArrayType(context, type) && isNumberType(context, argumentType)) { + return addToNumericExpression(index, 1); + } + + return index; +} + export const transformElementAccessExpression: FunctionVisitor = (node, context) => { const constEnumValue = tryGetConstEnumValue(context, node); if (constEnumValue) { return constEnumValue; } - const table = context.transformExpression(node.expression); + let [table, accessExpression] = transformOrderedExpressions(context, [node.expression, node.argumentExpression]); const type = context.checker.getTypeAtLocation(node.expression); const argumentType = context.checker.getTypeAtLocation(node.argumentExpression); if (isStringType(context, type) && isNumberType(context, argumentType)) { - const index = context.transformExpression(node.argumentExpression); - return transformLuaLibFunction(context, LuaLibFeature.StringAccess, node, table, index); + return transformLuaLibFunction(context, LuaLibFeature.StringAccess, node, table, accessExpression); } - const accessExpression = transformElementAccessArgument(context, node); + accessExpression = getElementAccessArgument(context, node, accessExpression); if (isMultiReturnCall(context, node.expression)) { const accessType = context.checker.getTypeAtLocation(node.argumentExpression); diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index 19f1e9b04..c2499480a 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -160,7 +160,7 @@ export function transformAssignmentExpression( context.addPrecedingStatements([ ...precedingStatements, lua.createVariableDeclarationStatement(tempVar, right, expression.right), - lua.createAssignmentStatement(left, lua.cloneIdentifier(tempVar, expression.left)), + lua.createAssignmentStatement(left, lua.cloneIdentifier(tempVar), expression.left), ]); return lua.cloneIdentifier(tempVar); } else { diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 0af1273bd..4ef7719bc 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -14,6 +14,7 @@ import { unwrapCompoundAssignmentToken, } from "./compound"; import { assert } from "../../../utils"; +import { transformOrderedExpressions } from "../expression-list"; type SimpleOperator = | ts.AdditiveOperatorOrHigher @@ -168,21 +169,7 @@ export const transformBinaryExpression: FunctionVisitor = ( } } - const lhs = context.transformExpression(node.left); - context.pushPrecedingStatements(); - const rhs = context.transformExpression(node.right); - const precedingStatements = context.popPrecedingStatements(); - - // Cache left in temp if right had preceding statements that may have referenced things in the left - if (precedingStatements.length > 0) { - const tempVar = context.createTempForNode(node.left); - context.addPrecedingStatements([ - lua.createVariableDeclarationStatement(tempVar, lhs, node.left), - ...precedingStatements, - ]); - return transformBinaryOperation(context, tempVar, rhs, operator, node); - } - + const [lhs, rhs] = transformOrderedExpressions(context, [node.left, node.right]); return transformBinaryOperation(context, lhs, rhs, operator, node); }; diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index a7c6a56cd..25217683b 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -23,7 +23,7 @@ import { transformTableSetExpression, } from "./language-extensions/table"; import { annotationRemoved, invalidTableDeleteExpression, invalidTableSetExpression } from "../utils/diagnostics"; -import { transformExpressionList } from "./expression-list"; +import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; export type PropertyCallExpression = ts.CallExpression & { expression: ts.PropertyAccessExpression }; @@ -33,13 +33,18 @@ export function transformArguments( signature?: ts.Signature, callContext?: ts.Expression ): lua.Expression[] { + context.pushPrecedingStatements(); const parameters = transformExpressionList(context, params); + const parametersPrecedingStatements = context.popPrecedingStatements(); // Add context as first param if present if (callContext) { parameters.unshift(context.transformExpression(callContext)); } + // Defer parameter preceding statements in case transforming the context arg generates some as well + context.addPrecedingStatements(parametersPrecedingStatements); + if (signature && signature.parameters.length >= params.length) { for (const [index, param] of params.entries()) { const signatureParameter = signature.parameters[index]; @@ -54,6 +59,50 @@ export function transformArguments( return parameters; } +function transformCallWithArgPrecedingStatements( + context: TransformationContext, + callExpression: ts.Expression, + args: lua.Expression[], + argPrecedingStatements: lua.Statement[], + callContext?: ts.Expression +): [lua.Expression, lua.Expression[]] { + let call = context.transformExpression(callExpression); + + args = args.slice(); + + // Transform and inject context if given one + if (callContext) { + context.pushPrecedingStatements(); + const transformedContext = context.transformExpression(callContext); + argPrecedingStatements = [...context.popPrecedingStatements(), ...argPrecedingStatements]; + args.unshift(transformedContext); + } + + // Cache call expression and context arg in temps to preserve execution order + if (argPrecedingStatements.length > 0) { + call = moveToPrecedingTemp(context, call); + if (callContext) { + args[0] = moveToPrecedingTemp(context, args[0]); + } + context.addPrecedingStatements(argPrecedingStatements); + } + + return [call, args]; +} + +export function transformCallAndArguments( + context: TransformationContext, + callExpression: ts.Expression, + params: readonly ts.Expression[], + signature?: ts.Signature, + callContext?: ts.Expression +): [lua.Expression, lua.Expression[]] { + context.pushPrecedingStatements(); + const args = transformArguments(context, params, signature, callContext); + const precedingStatements = context.popPrecedingStatements(); + return transformCallWithArgPrecedingStatements(context, callExpression, args, precedingStatements); +} + function transformElementAccessCall( context: TransformationContext, left: ts.PropertyAccessExpression | ts.ElementAccessExpression, @@ -87,7 +136,17 @@ export function transformContextualCallExpression( signature?: ts.Signature ): lua.CallExpression | lua.MethodCallExpression { const left = ts.isCallExpression(node) ? node.expression : node.tag; - if (ts.isPropertyAccessExpression(left) && ts.isIdentifier(left.name) && isValidLuaIdentifier(left.name.text)) { + + context.pushPrecedingStatements(); + const transformedArguments = transformArguments(context, args, signature); + const argPrecedingStatements = context.popPrecedingStatements(); + + if ( + ts.isPropertyAccessExpression(left) && + ts.isIdentifier(left.name) && + isValidLuaIdentifier(left.name.text) && + argPrecedingStatements.length === 0 + ) { // table:name() const table = context.transformExpression(left.expression); @@ -99,13 +158,13 @@ export function transformContextualCallExpression( table, lua.createStringLiteral(left.name.text, left.name), lua.createBooleanLiteral(node.questionDotToken !== undefined), // Require method is present if no ?.() call - ...transformArguments(context, args, signature) + ...transformedArguments ); } else { return lua.createMethodCallExpression( table, lua.createIdentifier(left.name.text, left.name), - transformArguments(context, args, signature), + transformedArguments, node ); } @@ -120,16 +179,24 @@ export function transformContextualCallExpression( context.addPrecedingStatements([selfAssignment]); return callExpression; } else { - const callContext = context.transformExpression(left.expression); - const expression = context.transformExpression(left); - const transformedArguments = transformArguments(context, args, signature); - return lua.createCallExpression(expression, [callContext, ...transformedArguments]); + const [expression, updatedArgs] = transformCallWithArgPrecedingStatements( + context, + left, + transformedArguments, + argPrecedingStatements, + left.expression + ); + return lua.createCallExpression(expression, updatedArgs, node); } } else if (ts.isIdentifier(left)) { - const callContext = context.isStrict ? ts.factory.createNull() : ts.factory.createIdentifier("_G"); - const transformedArguments = transformArguments(context, args, signature, callContext); - const expression = context.transformExpression(left); - return lua.createCallExpression(expression, transformedArguments, node); + const callContext = context.isStrict ? lua.createNilLiteral() : lua.createIdentifier("_G"); + const [expression, updatedArgs] = transformCallWithArgPrecedingStatements( + context, + left, + transformedArguments, + argPrecedingStatements + ); + return lua.createCallExpression(expression, [callContext, ...updatedArgs], node); } else { throw new Error(`Unsupported LeftHandSideExpression kind: ${ts.SyntaxKind[left.kind]}`); } @@ -153,8 +220,7 @@ function transformPropertyCall( return transformContextualCallExpression(context, node, node.arguments, signature); } else { // table.name() - const callPath = context.transformExpression(node.expression); - const parameters = transformArguments(context, node.arguments, signature); + const [callPath, parameters] = transformCallAndArguments(context, node.expression, node.arguments, signature); if (ts.isOptionalChain(node)) { return transformLuaLibFunction(context, LuaLibFeature.OptionalFunctionCall, node, callPath, ...parameters); @@ -175,8 +241,7 @@ function transformElementCall( return transformContextualCallExpression(context, node, node.arguments, signature); } else { // No context - const expression = context.transformExpression(node.expression); - const parameters = transformArguments(context, node.arguments, signature); + const [expression, parameters] = transformCallAndArguments(context, node.expression, node.arguments, signature); return lua.createCallExpression(expression, parameters); } } @@ -250,15 +315,21 @@ export const transformCallExpression: FunctionVisitor = (node ); } - const callPath = context.transformExpression(node.expression); const signatureDeclaration = signature?.getDeclaration(); + let callPath: lua.Expression; let parameters: lua.Expression[] = []; if (signatureDeclaration && getDeclarationContextType(context, signatureDeclaration) === ContextType.Void) { - parameters = transformArguments(context, node.arguments, signature); + [callPath, parameters] = transformCallAndArguments(context, node.expression, node.arguments, signature); } else { const callContext = context.isStrict ? ts.factory.createNull() : ts.factory.createIdentifier("_G"); - parameters = transformArguments(context, node.arguments, signature, callContext); + [callPath, parameters] = transformCallAndArguments( + context, + node.expression, + node.arguments, + signature, + callContext + ); } const callExpression = lua.createCallExpression(callPath, parameters, node); diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 0f4730343..b051da641 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -16,6 +16,16 @@ function isPrecedingStatementTemp(info: ExpressionListInfo) { return info.precedingStatements.length > 0 && lua.isIdentifier(info.transformedExpression); } +export function moveToPrecedingTemp(context: TransformationContext, expression: lua.Expression) { + const tempIdentifier = context.createTempForLuaExpression(expression); + const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression); + lua.setNodePosition(tempDeclaration, lua.getOriginalPos(expression)); + context.addPrecedingStatements([tempDeclaration]); + const tempClone = lua.cloneIdentifier(tempIdentifier); + lua.setNodePosition(tempClone, lua.getOriginalPos(tempIdentifier)); + return tempClone; +} + function processPrecedingStatements( context: TransformationContext, expressionInfo: ExpressionListInfo[], @@ -44,19 +54,14 @@ function processPrecedingStatements( } // Strip 'unpack' from spreads to store in a temp - it will be added back in buildArrayConcatCall, if needed - let expression = info.transformedExpression; if (info.isSpread) { - assert(lua.isCallExpression(expression) && expression.params.length === 1); - expression = expression.params[0]; + assert(lua.isCallExpression(info.transformedExpression) && info.transformedExpression.params.length === 1); + info.transformedExpression = info.transformedExpression.params[0]; info.needsUnpack = true; } // Inject temp assignment in correct place in preceding statements - const tempVar = context.createTempForLuaExpression(info.transformedExpression); - const tempDeclaration = lua.createVariableDeclarationStatement(tempVar, expression); - lua.setNodePosition(tempDeclaration, lua.getOriginalPos(expression)); - context.addPrecedingStatements([tempDeclaration]); - info.transformedExpression = lua.cloneIdentifier(tempVar); + info.transformedExpression = moveToPrecedingTemp(context, info.transformedExpression); } } @@ -112,3 +117,49 @@ export function transformExpressionList( return expressionInfo.map(e => e.transformedExpression); } + +export function transformOrderedExpressions( + context: TransformationContext, + expressions: ts.Expression[] +): lua.Expression[] { + const transformedExpressions: lua.Expression[] = []; + const precedingStatements: lua.Statement[][] = []; + let lastPrecedingStatementsIndex = -1; + for (let i = 0; i < expressions.length; ++i) { + context.pushPrecedingStatements(); + transformedExpressions.push(context.transformExpression(expressions[i])); + const expressionPrecedingStatements = context.popPrecedingStatements(); + precedingStatements.push(expressionPrecedingStatements); + if (expressionPrecedingStatements.length > 0) { + lastPrecedingStatementsIndex = i; + } + } + + if (lastPrecedingStatementsIndex < 0) { + return transformedExpressions; + } + + for (let i = 0; i < transformedExpressions.length; ++i) { + const transformedExpression = transformedExpressions[i]; + const expressionPrecedingStatements = precedingStatements[i]; + + // Bubble up preceding statements + context.addPrecedingStatements(expressionPrecedingStatements); + + // Cache expression in temp to maintain execution order, unless: + // - Expression is after the last one in the list which generated preceding statements + // - Expression is a literal that wouldn't be affected by preceding statements + // - Expression is a temp identifier which is a result of preceding statements + if ( + i >= lastPrecedingStatementsIndex || + lua.isLiteral(transformedExpression) || + (expressionPrecedingStatements.length > 0 && lua.isIdentifier(transformedExpression)) + ) { + continue; + } + + transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression); + } + + return transformedExpressions; +} diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 258175f9e..1b14c6431 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -9,7 +9,7 @@ import { createSafeName, hasUnsafeIdentifierName, hasUnsafeSymbolName } from ".. import { getSymbolIdOfSymbol, trackSymbolReference } from "../utils/symbols"; import { isArrayType } from "../utils/typescript"; import { transformFunctionLikeDeclaration } from "./function"; -import { transformExpressionList } from "./expression-list"; +import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; import { findMultiAssignmentViolations } from "./language-extensions/multi"; import { formatJSXStringValueLiteral } from "./jsx/jsx"; @@ -159,16 +159,10 @@ const transformObjectLiteralExpressionOrJsxAttributes: FunctionVisitor 0 && lua.isIdentifier(property.value)) ) { - const tempVar = context.createTempForLuaExpression(property.value); - context.addPrecedingStatements([ - lua.createVariableDeclarationStatement(tempVar, property.value), - ]); - property.value = lua.cloneIdentifier(tempVar); + property.value = moveToPrecedingTemp(context, property.value); } } else { - const tempVar = context.createTempForLuaExpression(property); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, property)]); - properties[i] = lua.cloneIdentifier(tempVar); + properties[i] = moveToPrecedingTemp(context, property); } } } diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index bc4005b8b..2b1082e73 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -114,11 +114,13 @@ describe("execution order", () => { test("tagged template literal", () => { util.testFunction` + let i = 0; + function func(strings: TemplateStringsArray, ...expressions: any[]) { - return { strings: [...strings], raw: strings.raw, expressions }; + const x = i > 0 ? "a" : "b"; + return { strings: [x, ...strings], raw: strings.raw, expressions }; } - let i = 0; return func\`hello \${i} \${i++}\`; `.expectToMatchJsResult(); }); @@ -130,79 +132,92 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); + test("index expression", () => { + util.testFunction` + let i = 0; + const a = [["A1", "A2"], ["B1", "B2"]]; + const result = a[i][i++]; + return [result, i]; + `.expectToMatchJsResult(); + }); + test("index assignment statement", () => { util.testFunction` let i = 0; - const a = [9, 8, 7]; + const a = [4, 5]; a[i] = i++; - return a; + return [a, i]; `.expectToMatchJsResult(); }); test("index assignment expression", () => { util.testFunction` let i = 0; - const a = [9, 8, 7]; - const x = a[i] = i++; - return a; + const a = [9, 8]; + const result = a[i] = i++; + return [result, a, i]; `.expectToMatchJsResult(); }); test("indirect index assignment statement", () => { util.testFunction` - let i = 1; - const a = [9, 8, 7]; - function foo(x: number) { i += x; return a; } + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { if (x > 0) { return a; } else { return b; } } foo(i)[i] = i++; - return a; + return [a, b, i]; `.expectToMatchJsResult(); }); test("indirect index assignment expression", () => { util.testFunction` - let i = 1; - const a = [9, 8, 7]; - function foo(x: number) { i += x; return a; } - const x = foo(i)[i] = i++; - return a; + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { if (x > 0) { return a; } else { return b; } } + const result = foo(i)[i] = i++; + return [result, a, b, i]; `.expectToMatchJsResult(); }); test("compound index assignment statement", () => { util.testFunction` let i = 0; - const a = [9, 8, 7]; + const a = [9, 8]; a[i] += i++; - return a; + return [a, i]; `.expectToMatchJsResult(); }); test("compound index assignment expression", () => { util.testFunction` let i = 0; - const a = [9, 8, 7]; - const x = a[i] += i++; - return a; + const a = [9, 8]; + const result = a[i] += i++; + return [result, a, i]; `.expectToMatchJsResult(); }); test("compound indirect index assignment statement", () => { util.testFunction` - let i = 1; - const a = [9, 8, 7]; - function foo(x: number) { i += x; return a; } + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { if (x > 0) { return a; } else { return b; } } foo(i)[i] += i++; - return a; + return [a, b, i]; `.expectToMatchJsResult(); }); test("compound indirect index assignment expression", () => { util.testFunction` let i = 1; - const a = [9, 8, 7]; - function foo(x: number) { i += x; return a; } - const x = foo(i)[i] += i++; - return a; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { if (x > 0) { return a; } else { return b; } } + const result = foo(i)[i] += i++; + return [result, a, b, i]; `.expectToMatchJsResult(); }); @@ -224,23 +239,144 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); - test("call statement", () => { + test("call expression", () => { util.testFunction` let i = 1; - const a = [9, 8, 7, 6, 5, 4, 3, 2, 1]; - function foo(x: number) { i += x; return ((y: number) => { i += y; return a; }); } - foo(i++)(i++)[i] = 0; - return a; + function a(x: number) { return x * 10; } + function b(x: number) { return x * 100; } + function foo(x: number) { if (x > 0) { return a; } else { return b; } } + const result = foo(i)(i++); + return [result, i]; `.expectToMatchJsResult(); }); - test("call expression", () => { + test("call expression (function modified)", () => { util.testFunction` let i = 1; - const a = [9, 8, 7, 6, 5, 4, 3, 2, 1]; - function foo(x: number) { i += x; return ((y: number) => { i += y; return a; }); } - const x = foo(i++)(i++)[i] = 0; - return a; + let foo = (x: null, y: number) => { return y; }; + function bar() { + foo = (x: null, y: number) => { return y * 10; }; + return null; + } + const result = foo(bar(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method call expression (method modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeFoo(this: void) { + o.foo = function(x: null, y: number) { return (y + this.val) * 10; }; + return null; + } + const result = o.foo(changeFoo(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method element access call expression (method modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeFoo(this: void) { + o.foo = function(x: null, y: number) { return (y + this.val) * 10; }; + return null; + } + function getFoo() { return "foo"; } + function getO() { return o; } + const result = getO()[getFoo()](changeFoo(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method call expression (object modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeO(this: void) { + o = { + val: 5, + foo: function(x: null, y: number) { return (y + this.val) * 10; } + }; + return null; + } + const result = o.foo(changeO(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method element access call expression (object modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeO(this: void) { + o = { + val: 5, + foo: function(x: null, y: number) { return (y + this.val) * 10; } + }; + return null; + } + function getFoo() { return "foo"; } + function getO() { return o; } + const result = getO()[getFoo()](changeO(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("array method call", () => { + util.testFunction` + let a = [7]; + let b = [9]; + function foo(x: number) { if (x > 0) { return b; } else { return a; } } + let i = 0; + foo(i).push(i, i++, i); + return [a, b, i]; + `.expectToMatchJsResult(); + }); + + test("function method call", () => { + util.testFunction` + let o = {val: 3}; + let a = function(x: number) { return this.val + x; }; + let b = function(x: number) { return (this.val + x) * 10; }; + function foo(x: number) { if (x > 0) { return b; } else { return a; } } + let i = 0; + const result = foo(i).call(o, i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("string method call", () => { + util.testFunction` + function foo(x: number) { if (x > 0) { return "foo"; } else { return "bar"; } } + let i = 0; + const result = foo(i).substr(++i); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("new call", () => { + util.testFunction` + class A { public val = 3; constructor(x: number) { this.val += x; } }; + class B { public val = 5; constructor(x: number) { this.val += (x * 10); } }; + function foo(x: number) { if (x > 0) { return B; } else { return A; } } + let i = 0; + const result = new (foo(i))(i++).val; + return [result, i]; `.expectToMatchJsResult(); }); }); From 54f61e6a490508e4932f86471f511f7031ca86b2 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 6 Sep 2021 16:25:22 -0600 Subject: [PATCH 19/51] more execution order fixes, more tests --- src/transformation/builtins/array.ts | 5 +-- src/transformation/builtins/function.ts | 5 +-- src/transformation/builtins/string.ts | 5 +-- src/transformation/visitors/call.ts | 43 +++++++++++-------- src/transformation/visitors/class/new.ts | 13 +++--- src/transformation/visitors/template.ts | 10 ++++- .../__snapshots__/deprecated.spec.ts.snap | 3 +- .../__snapshots__/iterable.spec.ts.snap | 6 ++- .../__snapshots__/table.spec.ts.snap | 10 +++-- test/unit/precedingStatements.spec.ts | 29 +++++++++++-- 10 files changed, 83 insertions(+), 46 deletions(-) diff --git a/src/transformation/builtins/array.ts b/src/transformation/builtins/array.ts index d8bd4dd3b..7bbd4cf98 100644 --- a/src/transformation/builtins/array.ts +++ b/src/transformation/builtins/array.ts @@ -3,7 +3,7 @@ import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; import { unsupportedProperty } from "../utils/diagnostics"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; -import { PropertyCallExpression, transformArguments } from "../visitors/call"; +import { PropertyCallExpression, transformArguments, transformCallAndArguments } from "../visitors/call"; import { isStringType, isNumberType } from "../utils/typescript"; export function transformArrayConstructorCall( @@ -29,8 +29,7 @@ export function transformArrayPrototypeCall( ): lua.CallExpression | undefined { const expression = node.expression; const signature = context.checker.getResolvedSignature(node); - const params = transformArguments(context, node.arguments, signature); - const caller = context.transformExpression(expression.expression); + const [caller, params] = transformCallAndArguments(context, expression.expression, node.arguments, signature); const expressionName = expression.name.text; switch (expressionName) { diff --git a/src/transformation/builtins/function.ts b/src/transformation/builtins/function.ts index f6cb8a6a0..aefe2a13e 100644 --- a/src/transformation/builtins/function.ts +++ b/src/transformation/builtins/function.ts @@ -6,7 +6,7 @@ import { unsupportedForTarget, unsupportedProperty, unsupportedSelfFunctionConve import { ContextType, getFunctionContextType } from "../utils/function-context"; import { createUnpackCall } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; -import { PropertyCallExpression, transformArguments } from "../visitors/call"; +import { PropertyCallExpression, transformCallAndArguments } from "../visitors/call"; export function transformFunctionPrototypeCall( context: TransformationContext, @@ -19,8 +19,7 @@ export function transformFunctionPrototypeCall( } const signature = context.checker.getResolvedSignature(node); - const params = transformArguments(context, node.arguments, signature); - const caller = context.transformExpression(expression.expression); + const [caller, params] = transformCallAndArguments(context, expression.expression, node.arguments, signature); const expressionName = expression.name.text; switch (expressionName) { case "apply": diff --git a/src/transformation/builtins/string.ts b/src/transformation/builtins/string.ts index 4cb21ea93..faa3af2bc 100644 --- a/src/transformation/builtins/string.ts +++ b/src/transformation/builtins/string.ts @@ -4,7 +4,7 @@ import { TransformationContext } from "../context"; import { unsupportedProperty } from "../utils/diagnostics"; import { addToNumericExpression, createNaN, getNumberLiteralValue } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; -import { PropertyCallExpression, transformArguments } from "../visitors/call"; +import { PropertyCallExpression, transformArguments, transformCallAndArguments } from "../visitors/call"; function createStringCall(methodName: string, tsOriginal: ts.Node, ...params: lua.Expression[]): lua.CallExpression { const stringIdentifier = lua.createIdentifier("string"); @@ -21,8 +21,7 @@ export function transformStringPrototypeCall( ): lua.Expression | undefined { const expression = node.expression; const signature = context.checker.getResolvedSignature(node); - const params = transformArguments(context, node.arguments, signature); - const caller = context.transformExpression(expression.expression); + const [caller, params] = transformCallAndArguments(context, expression.expression, node.arguments, signature); const expressionName = expression.name.text; switch (expressionName) { diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 25217683b..1a74ca45d 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -108,8 +108,22 @@ function transformElementAccessCall( left: ts.PropertyAccessExpression | ts.ElementAccessExpression, args: ts.Expression[] | ts.NodeArray, signature?: ts.Signature -): { statements: lua.Statement; result: lua.CallExpression } { +) { + // Cache left-side if it has effects + // local ____self = context; return ____self[argument](parameters); const selfIdentifier = lua.createIdentifier(context.createTempName("self")); + const callContext = context.transformExpression(left.expression); + const selfAssignment = lua.createVariableDeclarationStatement(selfIdentifier, callContext); + context.addPrecedingStatements([selfAssignment]); + + const argument = ts.isElementAccessExpression(left) + ? transformElementAccessArgument(context, left) + : lua.createStringLiteral(left.name.text); + + let index: lua.Expression = lua.createTableIndexExpression(selfIdentifier, argument); + + context.pushPrecedingStatements(); + const transformedArguments = transformArguments( context, args, @@ -117,16 +131,14 @@ function transformElementAccessCall( ts.factory.createIdentifier(selfIdentifier.text) ); - // Cache left-side if it has effects - // (function() local ____self = context; return ____self[argument](parameters); end)() - const argument = ts.isElementAccessExpression(left) - ? transformElementAccessArgument(context, left) - : lua.createStringLiteral(left.name.text); - const callContext = context.transformExpression(left.expression); - const selfAssignment = lua.createVariableDeclarationStatement(selfIdentifier, callContext); - const index = lua.createTableIndexExpression(selfIdentifier, argument); - const callExpression = lua.createCallExpression(index, transformedArguments); - return { statements: selfAssignment, result: callExpression }; + const argPrecedingStatements = context.popPrecedingStatements(); + if (argPrecedingStatements.length > 0) { + // Cache index in temp if args had preceding statements + index = moveToPrecedingTemp(context, index); + context.addPrecedingStatements(argPrecedingStatements); + } + + return lua.createCallExpression(index, transformedArguments); } export function transformContextualCallExpression( @@ -170,14 +182,7 @@ export function transformContextualCallExpression( } } else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) { if (isExpressionWithEvaluationEffect(left.expression)) { - const { statements: selfAssignment, result: callExpression } = transformElementAccessCall( - context, - left, - args, - signature - ); - context.addPrecedingStatements([selfAssignment]); - return callExpression; + return transformElementAccessCall(context, left, args, signature); } else { const [expression, updatedArgs] = transformCallWithArgPrecedingStatements( context, diff --git a/src/transformation/visitors/class/new.ts b/src/transformation/visitors/class/new.ts index 97e1c3407..f7433e7b5 100644 --- a/src/transformation/visitors/class/new.ts +++ b/src/transformation/visitors/class/new.ts @@ -4,7 +4,7 @@ import { FunctionVisitor, TransformationContext } from "../../context"; import { AnnotationKind, getTypeAnnotations } from "../../utils/annotations"; import { annotationInvalidArgumentCount, annotationRemoved } from "../../utils/diagnostics"; import { importLuaLibFeature, LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; -import { transformArguments } from "../call"; +import { transformArguments, transformCallAndArguments } from "../call"; import { isTableNewCall } from "../language-extensions/table"; const builtinErrorTypeNames = new Set([ @@ -61,14 +61,15 @@ export const transformNewExpression: FunctionVisitor = (node, } const signature = context.checker.getResolvedSignature(node); - const params = node.arguments - ? transformArguments(context, node.arguments, signature) - : [lua.createBooleanLiteral(true)]; + const [name, params] = transformCallAndArguments( + context, + node.expression, + node.arguments ?? [ts.factory.createTrue()], + signature + ); checkForLuaLibType(context, type); - const name = context.transformExpression(node.expression); - const customConstructorAnnotation = annotations.get(AnnotationKind.CustomConstructor); if (customConstructorAnnotation) { if (customConstructorAnnotation.args.length === 1) { diff --git a/src/transformation/visitors/template.ts b/src/transformation/visitors/template.ts index 9f9a19b33..5f949adb1 100644 --- a/src/transformation/visitors/template.ts +++ b/src/transformation/visitors/template.ts @@ -5,6 +5,7 @@ import { ContextType, getDeclarationContextType } from "../utils/function-contex import { wrapInToStringForConcat } from "../utils/lua-ast"; import { isStringType } from "../utils/typescript/types"; import { transformArguments, transformContextualCallExpression } from "./call"; +import { transformOrderedExpressions } from "./expression-list"; // TODO: Source positions function getRawLiteral(node: ts.LiteralLikeNode): string { @@ -24,8 +25,13 @@ export const transformTemplateExpression: FunctionVisitor parts.push(lua.createStringLiteral(head, node.head)); } - for (const span of node.templateSpans) { - const expression = context.transformExpression(span.expression); + const transformedExpressions = transformOrderedExpressions( + context, + node.templateSpans.map(s => s.expression) + ); + for (let i = 0; i < node.templateSpans.length; ++i) { + const span = node.templateSpans[i]; + const expression = transformedExpressions[i]; const spanType = context.checker.getTypeAtLocation(span.expression); if (isStringType(context, spanType)) { parts.push(expression); diff --git a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap index a962a4a30..17025dd09 100644 --- a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap +++ b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap @@ -66,9 +66,10 @@ function ____exports.__main(self) local function luaIter(self) local i = 0 return function() + local ____arr_1 = arr local ____i_0 = i i = ____i_0 + 1 - return arr[____i_0 + 1] + return ____arr_1[____i_0 + 1] end end local result = \\"\\" diff --git a/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap b/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap index d866c5e31..75d18e16b 100644 --- a/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap @@ -7,9 +7,10 @@ function ____exports.__main(self) local strsArray = {{\\"a1\\", \\"a2\\"}, {\\"b1\\", \\"b2\\"}, {\\"c1\\", \\"c2\\"}} local i = 0 return function() + local ____strsArray_1 = strsArray local ____i_0 = i i = ____i_0 + 1 - local strs = strsArray[____i_0 + 1] + local strs = ____strsArray_1[____i_0 + 1] if strs then return table.unpack(strs) end @@ -30,9 +31,10 @@ function ____exports.__main(self) local strsArray = {{\\"a1\\", \\"a2\\"}, {\\"b1\\", \\"b2\\"}, {\\"c1\\", \\"c2\\"}} local i = 0 return function() + local ____strsArray_1 = strsArray local ____i_0 = i i = ____i_0 + 1 - local strs = strsArray[____i_0 + 1] + local strs = ____strsArray_1[____i_0 + 1] if strs then return table.unpack(strs) end diff --git a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap index 76908a6e2..46d14f22b 100644 --- a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap @@ -68,8 +68,9 @@ foo = nil" exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = tableDelete({}, \\"foo\\");"): diagnostics 1`] = `"main.ts(3,25): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("declare function foo(arg: any): void; foo(tableDelete({}, \\"foo\\"));"): code 1`] = ` -"({}).foo = nil -foo(_G, nil)" +"local ____foo_0 = foo; +({}).foo = nil +____foo_0(_G, nil)" `; exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("declare function foo(arg: any): void; foo(tableDelete({}, \\"foo\\"));"): diagnostics 1`] = `"main.ts(3,55): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; @@ -96,8 +97,9 @@ foo = nil" exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = setTable({}, \\"foo\\", 3);"): diagnostics 1`] = `"main.ts(3,25): error TSTL: Table set extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("declare function foo(arg: any): void; foo(setTable({}, \\"foo\\", 3));"): code 1`] = ` -"({}).foo = 3 -foo(_G, nil)" +"local ____foo_0 = foo; +({}).foo = 3 +____foo_0(_G, nil)" `; exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("declare function foo(arg: any): void; foo(setTable({}, \\"foo\\", 3));"): diagnostics 1`] = `"main.ts(3,55): error TSTL: Table set extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`; diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 2b1082e73..e6042289d 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -108,7 +108,7 @@ describe("execution order", () => { test("template expression", () => { util.testFunction` let i = 0; - return \`\${i} - \${i++}\` + return \`\${i}, \${i++}, \${i}\`; `.expectToMatchJsResult(); }); @@ -239,6 +239,29 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); + test("object destructuring assignment statement", () => { + util.testFunction` + let i = "A"; + const o: Record = {ABCDE: "success"}; + function getO(x: string) { i = x + "C"; return o; } + function getI(x: string) { i = x + "E"; return i; } + const { [getI(i += "D")]: result } = getO(i += "B"); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment expression", () => { + util.testFunction` + let i = "A"; + const o: Record = {ABCDE: "success"}; + function getO(x: string) { i = x + "C"; return o; } + function getI(x: string) { i = x + "E"; return i; } + let result: string; + const x = ({ [getI(i += "D")]: result } = getO(i += "B")); + return [result, i]; + `.expectToMatchJsResult(); + }); + test("call expression", () => { util.testFunction` let i = 1; @@ -290,7 +313,7 @@ describe("execution order", () => { o.foo = function(x: null, y: number) { return (y + this.val) * 10; }; return null; } - function getFoo() { return "foo"; } + function getFoo() { return "foo" as const; } function getO() { return o; } const result = getO()[getFoo()](changeFoo(), i++); return [result, i]; @@ -330,7 +353,7 @@ describe("execution order", () => { }; return null; } - function getFoo() { return "foo"; } + function getFoo() { return "foo" as const; } function getO() { return o; } const result = getO()[getFoo()](changeO(), i++); return [result, i]; From e579ee2233d867fcdbf5565bd1d22e63c14ec20f Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 7 Sep 2021 18:36:29 -0600 Subject: [PATCH 20/51] working on fixes for destructuring exec order --- .../visitors/binary-expression/assignments.ts | 106 +++++++++++------- .../visitors/binary-expression/compound.ts | 38 +++++-- .../destructuring-assignments.ts | 70 +++++++++--- .../visitors/expression-list.ts | 2 +- src/transformation/visitors/loops/utils.ts | 2 +- .../__snapshots__/multi.spec.ts.snap | 8 +- test/unit/precedingStatements.spec.ts | 54 +++++++-- 7 files changed, 196 insertions(+), 84 deletions(-) diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index c2499480a..a318c792c 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -11,43 +11,41 @@ import { isArrayLength, transformDestructuringAssignment } from "./destructuring import { isMultiReturnCall } from "../language-extensions/multi"; import { notAllowedOptionalAssignment } from "../../utils/diagnostics"; import { transformElementAccessArgument } from "../access"; +import { transformOrderedExpressions } from "../expression-list"; export function transformAssignmentLeftHandSideExpression( context: TransformationContext, - node: ts.Expression + node: ts.Expression, + rightHasPrecedingStatements?: boolean ): lua.AssignmentLeftHandSideExpression { - const symbol = context.checker.getSymbolAtLocation(node); - const left = context.transformExpression(node); - - return lua.isIdentifier(left) && symbol && isSymbolExported(context, symbol) - ? createExportedIdentifier(context, left) - : cast(left, lua.isAssignmentLeftHandSideExpression); -} - -function transformAssignmentLeftHandSideExpressionWithRightPrecedingStatements( - context: TransformationContext, - expression: ts.Expression -) { // Cache index expression in a temp so it can be evaluated before right's preceding statements - if (ts.isElementAccessExpression(expression) && !ts.isLiteralExpression(expression.argumentExpression)) { - let table = context.transformExpression(expression.expression); + if ( + rightHasPrecedingStatements && + ts.isElementAccessExpression(node) && + !ts.isLiteralExpression(node.argumentExpression) + ) { + let table = context.transformExpression(node.expression); // If table is complex, it could reference things from the index expression and needs to be cached as well - if (!ts.isIdentifier(expression.expression)) { - const tableTemp = context.createTempForNode(expression.expression); - context.addPrecedingStatements([ - lua.createVariableDeclarationStatement(tableTemp, table, expression.expression), - ]); + if (!ts.isIdentifier(node.expression)) { + const tableTemp = context.createTempForNode(node.expression); + context.addPrecedingStatements([lua.createVariableDeclarationStatement(tableTemp, table, node.expression)]); table = lua.cloneIdentifier(tableTemp); } - const indexNode = expression.argumentExpression; + const indexNode = node.argumentExpression; const indexTemp = context.createTempForNode(indexNode); - const index = transformElementAccessArgument(context, expression); + const index = transformElementAccessArgument(context, node); context.addPrecedingStatements([lua.createVariableDeclarationStatement(indexTemp, index, indexNode)]); - return lua.createTableIndexExpression(table, lua.cloneIdentifier(indexTemp), expression); + return lua.createTableIndexExpression(table, lua.cloneIdentifier(indexTemp), node); } - return transformAssignmentLeftHandSideExpression(context, expression); + + const symbol = context.checker.getSymbolAtLocation(node); + const left = context.transformExpression(node); + + return lua.isIdentifier(left) && symbol && isSymbolExported(context, symbol) + ? createExportedIdentifier(context, left) + : cast(left, lua.isAssignmentLeftHandSideExpression); } export function transformAssignment( @@ -55,7 +53,7 @@ export function transformAssignment( // TODO: Change type to ts.LeftHandSideExpression? lhs: ts.Expression, right: lua.Expression, - rightPrecedingStatements?: lua.Statement[], + rightHasPrecedingStatements?: boolean, parent?: ts.Expression ): lua.Statement[] { if (ts.isOptionalChain(lhs)) { @@ -83,15 +81,11 @@ export function transformAssignment( const dependentSymbols = symbol ? getDependenciesOfSymbol(context, symbol) : []; - const left = - rightPrecedingStatements && rightPrecedingStatements.length > 0 - ? transformAssignmentLeftHandSideExpressionWithRightPrecedingStatements(context, lhs) - : transformAssignmentLeftHandSideExpression(context, lhs); + const left = transformAssignmentLeftHandSideExpression(context, lhs, rightHasPrecedingStatements); const rootAssignment = lua.createAssignmentStatement(left, right, lhs.parent); return [ - ...(rightPrecedingStatements ?? []), rootAssignment, ...dependentSymbols.map(symbol => { const [left] = rootAssignment.left; @@ -101,20 +95,36 @@ export function transformAssignment( ]; } +export function transformAssignmentWithRightPrecedingStatements( + context: TransformationContext, + lhs: ts.Expression, + right: lua.Expression, + rightPrecedingStatements: lua.Statement[], + parent?: ts.Expression +): lua.Statement[] { + return [ + ...rightPrecedingStatements, + ...transformAssignment(context, lhs, right, rightPrecedingStatements.length > 0, parent), + ]; +} + function transformDestructuredAssignmentExpression( context: TransformationContext, expression: ts.DestructuringAssignment ) { const rootIdentifier = context.createTempForNode(expression.right); + context.pushPrecedingStatements(); let right = context.transformExpression(expression.right); + const rootPrecedingStatements = context.popPrecedingStatements(); + context.addPrecedingStatements(rootPrecedingStatements); if (isMultiReturnCall(context, expression.right)) { right = wrapInTable(right); } const statements = [ lua.createVariableDeclarationStatement(rootIdentifier, right), - ...transformDestructuringAssignment(context, expression, rootIdentifier), + ...transformDestructuringAssignment(context, expression, rootIdentifier, rootPrecedingStatements.length > 0), ]; return { statements, result: rootIdentifier }; @@ -152,10 +162,11 @@ export function transformAssignmentExpression( const right = context.transformExpression(expression.right); const precedingStatements = context.popPrecedingStatements(); - const left = + const left = transformAssignmentLeftHandSideExpression( + context, + expression.left, precedingStatements.length > 0 - ? transformAssignmentLeftHandSideExpressionWithRightPrecedingStatements(context, expression.left) - : transformAssignmentLeftHandSideExpression(context, expression.left); + ); context.addPrecedingStatements([ ...precedingStatements, @@ -184,7 +195,7 @@ const canBeTransformedToLuaAssignmentStatement = ( } if (ts.isPropertyAccessExpression(element) || ts.isElementAccessExpression(element)) { - return true; + return false; } if (ts.isIdentifier(element)) { @@ -208,12 +219,15 @@ export function transformAssignmentStatement( if (isDestructuringAssignment(expression)) { if (canBeTransformedToLuaAssignmentStatement(context, expression)) { const rightType = context.checker.getTypeAtLocation(expression.right); - let right: lua.Expression | lua.Expression[] = context.transformExpression(expression.right); + let right: lua.Expression | lua.Expression[]; if (ts.isArrayLiteralExpression(expression.right)) { - right = expression.right.elements.map(e => context.transformExpression(e)); - } else if (!isMultiReturnCall(context, expression.right) && isArrayType(context, rightType)) { - right = createUnpackCall(context, right, expression.right); + right = transformOrderedExpressions(context, expression.right.elements); + } else { + right = context.transformExpression(expression.right); + if (!isMultiReturnCall(context, expression.right) && isArrayType(context, rightType)) { + right = createUnpackCall(context, right, expression.right); + } } const left = expression.left.elements.map(e => transformAssignmentLeftHandSideExpression(context, e)); @@ -221,20 +235,28 @@ export function transformAssignmentStatement( return [lua.createAssignmentStatement(left, right, expression)]; } + context.pushPrecedingStatements(); let right = context.transformExpression(expression.right); + const rootPrecedingStatements = context.popPrecedingStatements(); + context.addPrecedingStatements(rootPrecedingStatements); if (isMultiReturnCall(context, expression.right)) { right = wrapInTable(right); } - const rootIdentifier = lua.createAnonymousIdentifier(expression.left); + const rootIdentifier = context.createTempForNode(expression.left); return [ lua.createVariableDeclarationStatement(rootIdentifier, right), - ...transformDestructuringAssignment(context, expression, rootIdentifier), + ...transformDestructuringAssignment( + context, + expression, + rootIdentifier, + rootPrecedingStatements.length > 0 + ), ]; } else { context.pushPrecedingStatements(); const right = context.transformExpression(expression.right); const precedingStatements = context.popPrecedingStatements(); - return transformAssignment(context, expression.left, right, precedingStatements); + return transformAssignmentWithRightPrecedingStatements(context, expression.left, right, precedingStatements); } } diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index 501d3b1ce..2f79df6a7 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -3,7 +3,7 @@ import * as lua from "../../../LuaAST"; import { cast, assertNever } from "../../../utils"; import { TransformationContext } from "../../context"; import { transformBinaryOperation } from "../binary-expression"; -import { transformAssignment } from "./assignments"; +import { transformAssignmentWithRightPrecedingStatements } from "./assignments"; function isLuaExpressionWithSideEffect(expression: lua.Expression) { return !(lua.isLiteral(expression) || lua.isIdentifier(expression)); @@ -113,7 +113,12 @@ export function transformCompoundAssignment( const tmpIdentifier = context.createTempForLuaExpression(left); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); const operatorExpression = transformBinaryOperation(context, tmpIdentifier, right, operator, expression); - const assignStatements = transformAssignment(context, lhs, operatorExpression, rightPrecedingStatements); + const assignStatements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + rightPrecedingStatements + ); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else if (ts.isPropertyAccessExpression(lhs) || ts.isElementAccessExpression(lhs)) { // Simple property/element access expressions need to cache in temp to avoid double-evaluation @@ -132,14 +137,14 @@ export function transformCompoundAssignment( return { statements, result: tmpIdentifier }; } - const assignStatements = transformAssignment(context, lhs, tmpIdentifier, rightPrecedingStatements); + const assignStatements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + tmpIdentifier, + rightPrecedingStatements + ); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else { - // Simple expressions - // ${left} = ${left} ${operator} ${right} - const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); - const statements = transformAssignment(context, lhs, operatorExpression, rightPrecedingStatements); - if (rightPrecedingStatements.length > 0 && isSetterSkippingCompoundAssignmentOperator(operator)) { return { statements: transformSetterSkippingCompoundAssignment(left, operator, right, rightPrecedingStatements), @@ -147,6 +152,15 @@ export function transformCompoundAssignment( }; } + // Simple expressions + // ${left} = ${left} ${operator} ${right} + const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); + const statements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + rightPrecedingStatements + ); return { statements, result: left }; } } @@ -210,9 +224,13 @@ export function transformCompoundAssignmentStatement( // Simple statements // ${left} = ${left} ${replacementOperator} ${right} - const operatorExpression = transformBinaryOperation(context, left, right, operator, node); - return transformAssignment(context, lhs, operatorExpression, rightPrecedingStatements); + return transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + rightPrecedingStatements + ); } } diff --git a/src/transformation/visitors/binary-expression/destructuring-assignments.ts b/src/transformation/visitors/binary-expression/destructuring-assignments.ts index 567de4e6f..5dbc4da24 100644 --- a/src/transformation/visitors/binary-expression/destructuring-assignments.ts +++ b/src/transformation/visitors/binary-expression/destructuring-assignments.ts @@ -36,28 +36,31 @@ export function isArrayLength( export function transformDestructuringAssignment( context: TransformationContext, node: ts.DestructuringAssignment, - root: lua.Expression + root: lua.Expression, + rootHasPrecedingStatements: boolean ): lua.Statement[] { - return transformAssignmentPattern(context, node.left, root); + return transformAssignmentPattern(context, node.left, root, rootHasPrecedingStatements); } export function transformAssignmentPattern( context: TransformationContext, node: ts.AssignmentPattern, - root: lua.Expression + root: lua.Expression, + rootHasPrecedingStatements: boolean ): lua.Statement[] { switch (node.kind) { case ts.SyntaxKind.ObjectLiteralExpression: - return transformObjectLiteralAssignmentPattern(context, node, root); + return transformObjectLiteralAssignmentPattern(context, node, root, rootHasPrecedingStatements); case ts.SyntaxKind.ArrayLiteralExpression: - return transformArrayLiteralAssignmentPattern(context, node, root); + return transformArrayLiteralAssignmentPattern(context, node, root, rootHasPrecedingStatements); } } function transformArrayLiteralAssignmentPattern( context: TransformationContext, node: ts.ArrayLiteralExpression, - root: lua.Expression + root: lua.Expression, + rootHasPrecedingStatements: boolean ): lua.Statement[] { return node.elements.flatMap((element, index) => { const indexedRoot = lua.createTableIndexExpression(root, lua.createNumericLiteral(index + 1), element); @@ -67,16 +70,18 @@ function transformArrayLiteralAssignmentPattern( return transformObjectLiteralAssignmentPattern( context, element as ts.ObjectLiteralExpression, - indexedRoot + indexedRoot, + rootHasPrecedingStatements ); case ts.SyntaxKind.ArrayLiteralExpression: return transformArrayLiteralAssignmentPattern( context, element as ts.ArrayLiteralExpression, - indexedRoot + indexedRoot, + rootHasPrecedingStatements ); case ts.SyntaxKind.BinaryExpression: - const assignedVariable = lua.createIdentifier("____bindingAssignmentValue"); + const assignedVariable = context.createTempForLuaExpression(indexedRoot); const assignedVariableDeclaration = lua.createVariableDeclarationStatement( assignedVariable, @@ -89,12 +94,17 @@ function transformArrayLiteralAssignmentPattern( lua.SyntaxKind.EqualityOperator ); + context.pushPrecedingStatements(); + const defaultAssignmentStatements = transformAssignment( context, (element as ts.BinaryExpression).left, context.transformExpression((element as ts.BinaryExpression).right) ); + // Keep preceding statements inside if block + defaultAssignmentStatements.unshift(...context.popPrecedingStatements()); + const elseAssignmentStatements = transformAssignment( context, (element as ts.BinaryExpression).left, @@ -111,7 +121,9 @@ function transformArrayLiteralAssignmentPattern( case ts.SyntaxKind.Identifier: case ts.SyntaxKind.PropertyAccessExpression: case ts.SyntaxKind.ElementAccessExpression: - return transformAssignment(context, element, indexedRoot); + context.pushPrecedingStatements(); + const statements = transformAssignment(context, element, indexedRoot, rootHasPrecedingStatements); + return [...context.popPrecedingStatements(), ...statements]; // Keep preceding statements in order case ts.SyntaxKind.SpreadElement: if (index !== node.elements.length - 1) { // TypeScript error @@ -126,7 +138,14 @@ function transformArrayLiteralAssignmentPattern( lua.createNumericLiteral(index) ); - return transformAssignment(context, (element as ts.SpreadElement).expression, restElements); + context.pushPrecedingStatements(); + const spreadStatements = transformAssignment( + context, + (element as ts.SpreadElement).expression, + restElements, + rootHasPrecedingStatements + ); + return [...context.popPrecedingStatements(), ...spreadStatements]; // Keep preceding statements in order case ts.SyntaxKind.OmittedExpression: return []; default: @@ -139,7 +158,8 @@ function transformArrayLiteralAssignmentPattern( function transformObjectLiteralAssignmentPattern( context: TransformationContext, node: ts.ObjectLiteralExpression, - root: lua.Expression + root: lua.Expression, + rootHasPrecedingStatements: boolean ): lua.Statement[] { const result: lua.Statement[] = []; @@ -149,7 +169,7 @@ function transformObjectLiteralAssignmentPattern( result.push(...transformShorthandPropertyAssignment(context, property, root)); break; case ts.SyntaxKind.PropertyAssignment: - result.push(...transformPropertyAssignment(context, property, root)); + result.push(...transformPropertyAssignment(context, property, root, rootHasPrecedingStatements)); break; case ts.SyntaxKind.SpreadAssignment: result.push(...transformSpreadAssignment(context, property, root, node.properties)); @@ -207,7 +227,8 @@ function transformShorthandPropertyAssignment( function transformPropertyAssignment( context: TransformationContext, node: ts.PropertyAssignment, - root: lua.Expression + root: lua.Expression, + rootHasPrecedingStatements: boolean ): lua.Statement[] { const result: lua.Statement[] = []; @@ -216,11 +237,21 @@ function transformPropertyAssignment( const newRootAccess = lua.createTableIndexExpression(root, propertyAccessString); if (ts.isObjectLiteralExpression(node.initializer)) { - return transformObjectLiteralAssignmentPattern(context, node.initializer, newRootAccess); + return transformObjectLiteralAssignmentPattern( + context, + node.initializer, + newRootAccess, + rootHasPrecedingStatements + ); } if (ts.isArrayLiteralExpression(node.initializer)) { - return transformArrayLiteralAssignmentPattern(context, node.initializer, newRootAccess); + return transformArrayLiteralAssignmentPattern( + context, + node.initializer, + newRootAccess, + rootHasPrecedingStatements + ); } } @@ -228,7 +259,12 @@ function transformPropertyAssignment( const variableToExtract = transformPropertyName(context, node.name); const extractingExpression = lua.createTableIndexExpression(root, variableToExtract); - const destructureAssignmentStatements = transformAssignment(context, leftExpression, extractingExpression); + const destructureAssignmentStatements = transformAssignment( + context, + leftExpression, + extractingExpression, + rootHasPrecedingStatements + ); result.push(...destructureAssignmentStatements); diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index b051da641..d94edbd10 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -120,7 +120,7 @@ export function transformExpressionList( export function transformOrderedExpressions( context: TransformationContext, - expressions: ts.Expression[] + expressions: readonly ts.Expression[] ): lua.Expression[] { const transformedExpressions: lua.Expression[] = []; const precedingStatements: lua.Statement[][] = []; diff --git a/src/transformation/visitors/loops/utils.ts b/src/transformation/visitors/loops/utils.ts index c51a262be..ec3548baa 100644 --- a/src/transformation/visitors/loops/utils.ts +++ b/src/transformation/visitors/loops/utils.ts @@ -64,7 +64,7 @@ export function transformForInitializer( block.statements.unshift( ...(isAssignmentPattern(initializer) - ? transformAssignmentPattern(context, initializer, valueVariable) + ? transformAssignmentPattern(context, initializer, valueVariable, false) : transformAssignment(context, initializer, valueVariable)) ); } diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index 155203477..7d8e10091 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -180,10 +180,10 @@ local function multi(self, ...) return ... end local a -local ____ = { +local ____temp_0 = { ____(nil) } -a = ____[1] +a = ____temp_0[1] ____exports.a = a ____exports.a = a return ____exports" @@ -198,10 +198,10 @@ local function multi(self, ...) end local a do - local ____ = { + local ____temp_0 = { ____(nil, 1, 2) } - a = ____[1] + a = ____temp_0[1] ____exports.a = a while false do local ____ = 1 diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index e6042289d..fb27b8b75 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -221,21 +221,57 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); - test("destructuring assignment statement", () => { + test("array destructuring assignment statement", () => { util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; let i = 0; - const a = [9, 8, 7]; - [a[i++], a[i]] = [i++, i]; - return a; + [a[i], a[i++]] = [i++, i++]; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment expression", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + const x = [a[i], a[i++]] = [i++, i++]; + return [a, i, x]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment statement with default", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + [a[i] = i++, a[i++]] = [i++, i++]; + return [a, i]; `.expectToMatchJsResult(); }); - test("destructuring assignment expression", () => { + test("array destructuring assignment expression with default", () => { util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; let i = 0; - const a = [9, 8, 7]; - const x = [a[i++], a[i]] = [i++, i]; - return a; + const x = [a[i] = i++, a[i++]] = [i++, i++]; + return [a, i, x]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment statement with spread", () => { + util.testFunction` + let i = 0; + let a: number[][] = [[9, 9, 9], [9, 9, 9], [9, 9, 9]]; + [a[0][i], ...a[i++]] = [i++, i++]; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment expression with spread", () => { + util.testFunction` + let i = 0; + let a: number[][] = [[9, 9, 9], [9, 9, 9], [9, 9, 9]]; + const x = [a[0][i], ...a[i++]] = [i++, i++]; + return [a, i, x]; `.expectToMatchJsResult(); }); @@ -258,7 +294,7 @@ describe("execution order", () => { function getI(x: string) { i = x + "E"; return i; } let result: string; const x = ({ [getI(i += "D")]: result } = getO(i += "B")); - return [result, i]; + return [result, i, x]; `.expectToMatchJsResult(); }); From 897b21ccd0710d4a80a37874c8f272b2562abcd5 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Mon, 6 Sep 2021 20:34:39 -0700 Subject: [PATCH 21/51] Add benchmarks for all array functions --- .../runtime_benchmarks/array_concat_array.ts | 9 +++++++++ .../runtime_benchmarks/array_concat_spread.ts | 9 +++++++++ benchmark/src/runtime_benchmarks/array_every.ts | 7 +++++++ .../src/runtime_benchmarks/array_filter.ts | 7 +++++++ benchmark/src/runtime_benchmarks/array_find.ts | 9 +++++++++ .../src/runtime_benchmarks/array_findIndex.ts | 9 +++++++++ benchmark/src/runtime_benchmarks/array_flat.ts | 7 +++++++ .../src/runtime_benchmarks/array_flatMap.ts | 13 +++++++++++++ .../src/runtime_benchmarks/array_foreach.ts | 10 ++++++++++ .../src/runtime_benchmarks/array_includes.ts | 9 +++++++++ .../src/runtime_benchmarks/array_indexOf.ts | 9 +++++++++ benchmark/src/runtime_benchmarks/array_join.ts | 17 +++++++++++++++++ benchmark/src/runtime_benchmarks/array_map.ts | 7 +++++++ .../src/runtime_benchmarks/array_push_array.ts | 9 +++++++++ .../{array_push.ts => array_push_multiple.ts} | 4 ++-- .../src/runtime_benchmarks/array_push_single.ts | 8 ++++++++ .../src/runtime_benchmarks/array_reduce.ts | 7 +++++++ .../src/runtime_benchmarks/array_reduceRight.ts | 7 +++++++ .../src/runtime_benchmarks/array_reverse.ts | 7 +++++++ benchmark/src/runtime_benchmarks/array_slice.ts | 9 +++++++++ benchmark/src/runtime_benchmarks/array_some.ts | 7 +++++++ .../src/runtime_benchmarks/array_splice.ts | 9 +++++++++ .../src/runtime_benchmarks/array_unshift.ts | 9 +++++++++ 23 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 benchmark/src/runtime_benchmarks/array_concat_array.ts create mode 100644 benchmark/src/runtime_benchmarks/array_concat_spread.ts create mode 100644 benchmark/src/runtime_benchmarks/array_every.ts create mode 100644 benchmark/src/runtime_benchmarks/array_filter.ts create mode 100644 benchmark/src/runtime_benchmarks/array_find.ts create mode 100644 benchmark/src/runtime_benchmarks/array_findIndex.ts create mode 100644 benchmark/src/runtime_benchmarks/array_flat.ts create mode 100644 benchmark/src/runtime_benchmarks/array_flatMap.ts create mode 100644 benchmark/src/runtime_benchmarks/array_foreach.ts create mode 100644 benchmark/src/runtime_benchmarks/array_includes.ts create mode 100644 benchmark/src/runtime_benchmarks/array_indexOf.ts create mode 100644 benchmark/src/runtime_benchmarks/array_join.ts create mode 100644 benchmark/src/runtime_benchmarks/array_map.ts create mode 100644 benchmark/src/runtime_benchmarks/array_push_array.ts rename benchmark/src/runtime_benchmarks/{array_push.ts => array_push_multiple.ts} (68%) create mode 100644 benchmark/src/runtime_benchmarks/array_push_single.ts create mode 100644 benchmark/src/runtime_benchmarks/array_reduce.ts create mode 100644 benchmark/src/runtime_benchmarks/array_reduceRight.ts create mode 100644 benchmark/src/runtime_benchmarks/array_reverse.ts create mode 100644 benchmark/src/runtime_benchmarks/array_slice.ts create mode 100644 benchmark/src/runtime_benchmarks/array_some.ts create mode 100644 benchmark/src/runtime_benchmarks/array_splice.ts create mode 100644 benchmark/src/runtime_benchmarks/array_unshift.ts diff --git a/benchmark/src/runtime_benchmarks/array_concat_array.ts b/benchmark/src/runtime_benchmarks/array_concat_array.ts new file mode 100644 index 000000000..9c9a7e83d --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_concat_array.ts @@ -0,0 +1,9 @@ +export default function arrayConcat(): number[] { + const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const n = 50000; + for (let i = 0; i < n; i++) { + arr1.concat(arr2); + } + return arr1; +} diff --git a/benchmark/src/runtime_benchmarks/array_concat_spread.ts b/benchmark/src/runtime_benchmarks/array_concat_spread.ts new file mode 100644 index 000000000..a04eff958 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_concat_spread.ts @@ -0,0 +1,9 @@ +export default function arrayConcat(): number[] { + const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const n = 50000; + for (let i = 0; i < n; i++) { + arr1.concat(...arr2); + } + return arr1; +} diff --git a/benchmark/src/runtime_benchmarks/array_every.ts b/benchmark/src/runtime_benchmarks/array_every.ts new file mode 100644 index 000000000..f5dbfc4e4 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_every.ts @@ -0,0 +1,7 @@ +export default function arrayEvery() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.every((item, index) => item > index); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_filter.ts b/benchmark/src/runtime_benchmarks/array_filter.ts new file mode 100644 index 000000000..127d2476d --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_filter.ts @@ -0,0 +1,7 @@ +export default function arrayFilter() { + const n = 100000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.filter((item, index) => item > index); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_find.ts b/benchmark/src/runtime_benchmarks/array_find.ts new file mode 100644 index 000000000..8a84ff4e5 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_find.ts @@ -0,0 +1,9 @@ +export default function arrayFind() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.find(value => value === j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_findIndex.ts b/benchmark/src/runtime_benchmarks/array_findIndex.ts new file mode 100644 index 000000000..c058a99df --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_findIndex.ts @@ -0,0 +1,9 @@ +export default function arrayFindIndex() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.findIndex(value => value === j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_flat.ts b/benchmark/src/runtime_benchmarks/array_flat.ts new file mode 100644 index 000000000..36ace2d95 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_flat.ts @@ -0,0 +1,7 @@ +export default function arrayFlat() { + const n = 50000; + const array = [1, 2, [3, [4, 5], 6], 7, [8, 9], 10]; + for (let i = 0; i < n; i++) { + array.flat(2); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_flatMap.ts b/benchmark/src/runtime_benchmarks/array_flatMap.ts new file mode 100644 index 000000000..26c6490e5 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_flatMap.ts @@ -0,0 +1,13 @@ +export default function arrayFlatMap() { + const n = 50000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + array.flatMap((el, index) => { + if (index < 5) { + return [el, el + 1]; + } else { + return el + 2; + } + }); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_foreach.ts b/benchmark/src/runtime_benchmarks/array_foreach.ts new file mode 100644 index 000000000..8e14c8119 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_foreach.ts @@ -0,0 +1,10 @@ +export default function arrayForeach() { + const n = 200000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + array.forEach(value => { + let foo = value * 2; + foo = foo; + }); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_includes.ts b/benchmark/src/runtime_benchmarks/array_includes.ts new file mode 100644 index 000000000..828c167a3 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_includes.ts @@ -0,0 +1,9 @@ +export default function arrayIncludes() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.includes(j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_indexOf.ts b/benchmark/src/runtime_benchmarks/array_indexOf.ts new file mode 100644 index 000000000..333c67988 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_indexOf.ts @@ -0,0 +1,9 @@ +export default function arrayIndexOf() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.indexOf(j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_join.ts b/benchmark/src/runtime_benchmarks/array_join.ts new file mode 100644 index 000000000..10a96e590 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_join.ts @@ -0,0 +1,17 @@ +const array = [1, 2, "3", 4, 3, "6", { foo: 3 }, 8, 9, 10]; + +const k = 500; +for (let i = 0; i < k; i++) { + if (i % 2 === 0) { + array[i] = i.toString(); + } else { + array[i] = i % 3; + } +} + +export default function arrayJoin() { + const n = 3000; + for (let i = 0; i < n; i++) { + array.join("|"); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_map.ts b/benchmark/src/runtime_benchmarks/array_map.ts new file mode 100644 index 000000000..79d00d66a --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_map.ts @@ -0,0 +1,7 @@ +export default function arrayMap() { + const n = 100000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + array.map(value => value * 2); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_push_array.ts b/benchmark/src/runtime_benchmarks/array_push_array.ts new file mode 100644 index 000000000..fcc239a31 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_push_array.ts @@ -0,0 +1,9 @@ +export default function arrayPush(): number[] { + const n = 200000; + const numberList: number[] = []; + const numbers = [1, 2, 3]; + for (let i = 0; i < n; i++) { + numberList.push(...numbers); + } + return numberList; +} diff --git a/benchmark/src/runtime_benchmarks/array_push.ts b/benchmark/src/runtime_benchmarks/array_push_multiple.ts similarity index 68% rename from benchmark/src/runtime_benchmarks/array_push.ts rename to benchmark/src/runtime_benchmarks/array_push_multiple.ts index e0d2d0b87..dbef5460c 100644 --- a/benchmark/src/runtime_benchmarks/array_push.ts +++ b/benchmark/src/runtime_benchmarks/array_push_multiple.ts @@ -1,8 +1,8 @@ export default function arrayPush(): number[] { - const n = 1000000; + const n = 200000; const numberList: number[] = []; for (let i = 0; i < n; i++) { - numberList[numberList.length] = i * i; + numberList.push(i * i, i + 1); } return numberList; } diff --git a/benchmark/src/runtime_benchmarks/array_push_single.ts b/benchmark/src/runtime_benchmarks/array_push_single.ts new file mode 100644 index 000000000..f09bc21ec --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_push_single.ts @@ -0,0 +1,8 @@ +export default function arrayPush(): number[] { + const n = 500000; + const numberList: number[] = []; + for (let i = 0; i < n; i++) { + numberList.push(i * i); + } + return numberList; +} diff --git a/benchmark/src/runtime_benchmarks/array_reduce.ts b/benchmark/src/runtime_benchmarks/array_reduce.ts new file mode 100644 index 000000000..2d615e80f --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_reduce.ts @@ -0,0 +1,7 @@ +export default function arrayReduce() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.reduce((prev, cur, i) => prev + cur + i, 1); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_reduceRight.ts b/benchmark/src/runtime_benchmarks/array_reduceRight.ts new file mode 100644 index 000000000..9dd97d3d2 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_reduceRight.ts @@ -0,0 +1,7 @@ +export default function arrayReduce() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.reduceRight((prev, cur, i) => prev + cur + i, 1); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_reverse.ts b/benchmark/src/runtime_benchmarks/array_reverse.ts new file mode 100644 index 000000000..539d26138 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_reverse.ts @@ -0,0 +1,7 @@ +export default function arrayReverse(): void { + const n = 500000; + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + numbers.reverse(); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_slice.ts b/benchmark/src/runtime_benchmarks/array_slice.ts new file mode 100644 index 000000000..5419cb0d9 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_slice.ts @@ -0,0 +1,9 @@ +export default function arraySlice() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 6; j++) { + array.slice(j, -j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_some.ts b/benchmark/src/runtime_benchmarks/array_some.ts new file mode 100644 index 000000000..8add42a56 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_some.ts @@ -0,0 +1,7 @@ +export default function arraySome() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.some((item, index) => item < index); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_splice.ts b/benchmark/src/runtime_benchmarks/array_splice.ts new file mode 100644 index 000000000..4620bad86 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_splice.ts @@ -0,0 +1,9 @@ +export default function arraySplice() { + const n = 20000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.splice(j, 2, 1, 2); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_unshift.ts b/benchmark/src/runtime_benchmarks/array_unshift.ts new file mode 100644 index 000000000..b0f59a0b2 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_unshift.ts @@ -0,0 +1,9 @@ +export default function arrayUnshift(): number[] { + const n = 2000; + const numberList: number[] = []; + const numbers = [1, 2, 3]; + for (let i = 0; i < n; i++) { + numberList.unshift(...numbers); + } + return numberList; +} From b3f525f4948f87d038967b283f3b205b0a2f0c9b Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Thu, 9 Sep 2021 16:27:34 -0700 Subject: [PATCH 22/51] Optimize Array.push and add utilities for it --- src/LuaAST.ts | 41 ++++++++----- src/LuaPrinter.ts | 5 +- src/lualib/ArrayPush.ts | 10 +-- src/transformation/builtins/array.ts | 61 +++++++++++++++++-- src/transformation/utils/lua-ast.ts | 24 +++++++- src/transformation/visitors/class/index.ts | 2 +- .../visitors/class/members/accessors.ts | 2 +- .../visitors/class/members/constructor.ts | 2 +- .../visitors/expression-statement.ts | 4 +- src/transformation/visitors/function.ts | 6 +- test/transpile/module-resolution.spec.ts | 6 +- test/unit/builtins/array.spec.ts | 16 ++++- test/unit/builtins/loading.spec.ts | 4 +- 13 files changed, 139 insertions(+), 44 deletions(-) diff --git a/src/LuaAST.ts b/src/LuaAST.ts index ee5c9d329..85b25c956 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -86,6 +86,14 @@ export enum SyntaxKind { BitwiseNotOperator, // Unary } +export enum NodeFlags { + None = 0, + Inline = 1 << 0, // Keep function body on same line + Declaration = 1 << 1, // Prefer declaration syntax `function foo()` over assignment syntax `foo = function()` + PossiblyNotUsed = 1 << 2, // for an expression, if in an ExpressionStatement, statement is not emitted + IsUnpackCall = 1 << 3, +} + // TODO maybe name this PrefixUnary? not sure it makes sense to do so, because all unary ops in Lua are prefix export type UnaryBitwiseOperator = SyntaxKind.BitwiseNotOperator; @@ -132,18 +140,19 @@ export interface TextRange { export interface Node extends TextRange { kind: SyntaxKind; + flags: NodeFlags; } -export function createNode(kind: SyntaxKind, tsOriginal?: ts.Node): Node { +export function createNode(kind: SyntaxKind, tsOriginal?: ts.Node, flags: NodeFlags = NodeFlags.None): Node { if (tsOriginal === undefined) { - return { kind }; + return { kind, flags }; } const sourcePosition = getSourcePosition(tsOriginal); if (sourcePosition) { - return { kind, line: sourcePosition.line, column: sourcePosition.column }; + return { kind, flags, line: sourcePosition.line, column: sourcePosition.column }; } else { - return { kind }; + return { kind, flags }; } } @@ -151,6 +160,16 @@ export function cloneNode(node: T): T { return { ...node }; } +export function setNodeFlags(node: T, flags: NodeFlags): T { + node.flags = flags; + return node; +} + +export function addNodeFlags(node: T, flags: NodeFlags): T { + node.flags |= flags; + return node; +} + export function setNodePosition(node: T, position: TextRange): T { node.line = position.line; node.column = position.column; @@ -578,18 +597,11 @@ export function isLiteral( ); } -export enum FunctionExpressionFlags { - None = 1 << 0, - Inline = 1 << 1, // Keep function body on same line - Declaration = 1 << 2, // Prefer declaration syntax `function foo()` over assignment syntax `foo = function()` -} - export interface FunctionExpression extends Expression { kind: SyntaxKind.FunctionExpression; params?: Identifier[]; dots?: DotsLiteral; body: Block; - flags: FunctionExpressionFlags; } export function isFunctionExpression(node: Node): node is FunctionExpression { @@ -600,14 +612,13 @@ export function createFunctionExpression( body: Block, params?: Identifier[], dots?: DotsLiteral, - flags = FunctionExpressionFlags.None, + flags = NodeFlags.None, tsOriginal?: ts.Node ): FunctionExpression { - const expression = createNode(SyntaxKind.FunctionExpression, tsOriginal) as FunctionExpression; + const expression = createNode(SyntaxKind.FunctionExpression, tsOriginal, flags) as FunctionExpression; expression.body = body; expression.params = params; expression.dots = dots; - expression.flags = flags; return expression; } @@ -819,6 +830,6 @@ export function isInlineFunctionExpression(expression: FunctionExpression): expr expression.body.statements?.length === 1 && isReturnStatement(expression.body.statements[0]) && expression.body.statements[0].expressions !== undefined && - (expression.flags & FunctionExpressionFlags.Inline) !== 0 + (expression.flags & NodeFlags.Inline) !== 0 ); } diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index 65c33b88a..82ed0a6fa 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -392,10 +392,7 @@ export class LuaPrinter { chunks.push(this.indent()); - if ( - lua.isFunctionDefinition(statement) && - (statement.right[0].flags & lua.FunctionExpressionFlags.Declaration) !== 0 - ) { + if (lua.isFunctionDefinition(statement) && (statement.right[0].flags & lua.NodeFlags.Declaration) !== 0) { // Use `function foo()` instead of `foo = function()` const name = this.printExpression(statement.left[0]); if (isValidLuaFunctionDeclarationName(name.toString())) { diff --git a/src/lualib/ArrayPush.ts b/src/lualib/ArrayPush.ts index f09005e44..b1a1af52e 100644 --- a/src/lualib/ArrayPush.ts +++ b/src/lualib/ArrayPush.ts @@ -1,6 +1,8 @@ -function __TS__ArrayPush(this: void, arr: T[], ...items: T[]): number { - for (const item of items) { - arr[arr.length] = item; +function __TS__ArrayPush(this: void, arr: T[], items: T[]): number { + let len = arr.length; + for (const i of $range(1, items.length)) { + len++; + arr[len - 1] = items[i - 1]; } - return arr.length; + return len; } diff --git a/src/transformation/builtins/array.ts b/src/transformation/builtins/array.ts index 7bbd4cf98..ca986fdf1 100644 --- a/src/transformation/builtins/array.ts +++ b/src/transformation/builtins/array.ts @@ -5,6 +5,8 @@ import { unsupportedProperty } from "../utils/diagnostics"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { PropertyCallExpression, transformArguments, transformCallAndArguments } from "../visitors/call"; import { isStringType, isNumberType } from "../utils/typescript"; +import { moveToPrecedingTemp } from "../visitors/expression-list"; +import { wrapInReadonlyTable } from "../utils/lua-ast"; export function transformArrayConstructorCall( context: TransformationContext, @@ -23,10 +25,45 @@ export function transformArrayConstructorCall( } } +/** + * Optimized single element Array.push + * + * array[#array+1] = el + * return #array + */ +function transformSingleElementArrayPush( + context: TransformationContext, + node: PropertyCallExpression, + caller: lua.Expression, + param: lua.Expression +): lua.Expression { + const arrayIdentifier = lua.isIdentifier(caller) ? caller : moveToPrecedingTemp(context, caller); + + // #array + 1 + const lengthExpression = lua.createBinaryExpression( + lua.createUnaryExpression(arrayIdentifier, lua.SyntaxKind.LengthOperator), + lua.createNumericLiteral(1), + lua.SyntaxKind.AdditionOperator + ); + + // array[#array + 1] = + const pushStatement = lua.createAssignmentStatement( + lua.createTableIndexExpression(arrayIdentifier, lengthExpression), + param, + node + ); + context.addPrecedingStatements([pushStatement]); + + return lua.setNodeFlags( + lua.createUnaryExpression(arrayIdentifier, lua.SyntaxKind.LengthOperator), + lua.NodeFlags.PossiblyNotUsed + ); +} + export function transformArrayPrototypeCall( context: TransformationContext, node: PropertyCallExpression -): lua.CallExpression | undefined { +): lua.Expression | undefined { const expression = node.expression; const signature = context.checker.getResolvedSignature(node); const [caller, params] = transformCallAndArguments(context, expression.expression, node.arguments, signature); @@ -34,17 +71,33 @@ export function transformArrayPrototypeCall( const expressionName = expression.name.text; switch (expressionName) { case "concat": - return transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, node, caller, ...params); + return transformLuaLibFunction( + context, + LuaLibFeature.ArrayConcat, + node, + caller, + wrapInReadonlyTable(params) + ); case "entries": return transformLuaLibFunction(context, LuaLibFeature.ArrayEntries, node, caller); case "push": - return transformLuaLibFunction(context, LuaLibFeature.ArrayPush, node, caller, ...params); + if (node.arguments.length === 1 && !ts.isSpreadElement(node.arguments[0])) { + return transformSingleElementArrayPush(context, node, caller, params[0]); + } + + return transformLuaLibFunction(context, LuaLibFeature.ArrayPush, node, caller, wrapInReadonlyTable(params)); case "reverse": return transformLuaLibFunction(context, LuaLibFeature.ArrayReverse, node, caller); case "shift": return transformLuaLibFunction(context, LuaLibFeature.ArrayShift, node, caller); case "unshift": - return transformLuaLibFunction(context, LuaLibFeature.ArrayUnshift, node, caller, ...params); + return transformLuaLibFunction( + context, + LuaLibFeature.ArrayUnshift, + node, + caller, + wrapInReadonlyTable(params) + ); case "sort": return transformLuaLibFunction(context, LuaLibFeature.ArraySort, node, caller, ...params); case "pop": diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index 33f31ddae..668f15882 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -66,9 +66,12 @@ export function createUnpackCall( context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node -): lua.Expression { +): lua.CallExpression { if (context.luaTarget === LuaTarget.Universal) { - return transformLuaLibFunction(context, LuaLibFeature.Unpack, tsOriginal, expression); + return lua.setNodeFlags( + transformLuaLibFunction(context, LuaLibFeature.Unpack, tsOriginal, expression), + lua.NodeFlags.IsUnpackCall + ); } const unpack = @@ -76,7 +79,11 @@ export function createUnpackCall( ? lua.createIdentifier("unpack") : lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("unpack")); - return lua.createCallExpression(unpack, [expression], tsOriginal); + return lua.setNodeFlags(lua.createCallExpression(unpack, [expression], tsOriginal), lua.NodeFlags.IsUnpackCall); +} + +export function isUnpackCall(node: lua.Node): node is lua.CallExpression { + return lua.isCallExpression(node) && (node.flags & lua.NodeFlags.IsUnpackCall) !== 0; } export function wrapInTable(...expressions: lua.Expression[]): lua.TableExpression { @@ -84,6 +91,17 @@ export function wrapInTable(...expressions: lua.Expression[]): lua.TableExpressi return lua.createTableExpression(fields); } +/** + * If params is only one unpack call, then returns the unpacked table instead. + * So the resulting expression should only be used when guaranteed readonly. + */ +export function wrapInReadonlyTable(args: lua.Expression[]): lua.Expression { + if (args.length === 1 && isUnpackCall(args[0])) { + return args[0].params[0]; + } + return wrapInTable(...args); +} + export function wrapInToStringForConcat(expression: lua.Expression): lua.Expression { if ( lua.isStringLiteral(expression) || diff --git a/src/transformation/visitors/class/index.ts b/src/transformation/visitors/class/index.ts index 03bc661f3..74c20c06d 100644 --- a/src/transformation/visitors/class/index.ts +++ b/src/transformation/visitors/class/index.ts @@ -159,7 +159,7 @@ function transformClassLikeDeclaration( lua.createBlock(constructorBody), [createSelfIdentifier()], lua.createDotsLiteral(), - lua.FunctionExpressionFlags.Declaration + lua.NodeFlags.Declaration ); result.push( lua.createAssignmentStatement(createConstructorName(localClassName), constructorFunction, classDeclaration) diff --git a/src/transformation/visitors/class/members/accessors.ts b/src/transformation/visitors/class/members/accessors.ts index f774b9645..fe34a3d8a 100644 --- a/src/transformation/visitors/class/members/accessors.ts +++ b/src/transformation/visitors/class/members/accessors.ts @@ -11,7 +11,7 @@ import { createPrototypeName } from "./constructor"; function transformAccessor(context: TransformationContext, node: ts.AccessorDeclaration): lua.FunctionExpression { const [params, dot, restParam] = transformParameters(context, node.parameters, createSelfIdentifier()); const body = node.body ? transformFunctionBody(context, node.parameters, node.body, restParam)[0] : []; - return lua.createFunctionExpression(lua.createBlock(body), params, dot, lua.FunctionExpressionFlags.Declaration); + return lua.createFunctionExpression(lua.createBlock(body), params, dot, lua.NodeFlags.Declaration); } export function transformAccessorDeclarations( diff --git a/src/transformation/visitors/class/members/constructor.ts b/src/transformation/visitors/class/members/constructor.ts index 11766d2a8..22ab14feb 100644 --- a/src/transformation/visitors/class/members/constructor.ts +++ b/src/transformation/visitors/class/members/constructor.ts @@ -90,7 +90,7 @@ export function transformConstructorDeclaration( return lua.createAssignmentStatement( createConstructorName(className), - lua.createFunctionExpression(block, params, dotsLiteral, lua.FunctionExpressionFlags.Declaration), + lua.createFunctionExpression(block, params, dotsLiteral, lua.NodeFlags.Declaration), constructorWasGenerated ? classDeclaration : statement ); } diff --git a/src/transformation/visitors/expression-statement.ts b/src/transformation/visitors/expression-statement.ts index 228307f25..962b20a84 100644 --- a/src/transformation/visitors/expression-statement.ts +++ b/src/transformation/visitors/expression-statement.ts @@ -32,7 +32,9 @@ export const transformExpressionStatement: FunctionVisitor { const projectPath = path.resolve(__dirname, "module-resolution", "project-with-node-modules"); @@ -446,8 +446,8 @@ test("includes lualib_bundle when external lua requests it", () => { require("lualib_bundle") local result = {} - __TS__ArrayPush(result, "foo") - __TS__ArrayPush(result, "bar") + __TS__ArrayPush(result, { "foo" }) + __TS__ArrayPush(result, { "bar" }) return { foo = result } ` diff --git a/test/unit/builtins/array.spec.ts b/test/unit/builtins/array.spec.ts index 037ebe2a7..1e2fe734f 100644 --- a/test/unit/builtins/array.spec.ts +++ b/test/unit/builtins/array.spec.ts @@ -471,13 +471,25 @@ test.each([ util.testExpression`${util.formatCode(array)}.indexOf(${util.formatCode(...args)})`.expectToMatchJsResult(); }); -test.each([{ args: [1] }, { args: [1, 2, 3] }])("array.push (%p)", ({ args }) => { +test.each([{ args: "1" }, { args: "1, 2, 3" }, { args: "...[1, 2, 3]" }])("array.push (%p)", ({ args }) => { util.testFunction` const array = [0]; - const value = array.push(${util.formatCode(...args)}); + const value = array.push(${args}); return { array, value }; `.expectToMatchJsResult(); }); +test("array.push (optimized vararg)", () => { + util.testModule` + function pushAll(...args: any[]) { + const array = [0]; + const value = array.push(...args); + return { array, value }; + } + export const result = pushAll(1, 2, 3) + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); test.each([ { array: [1, 2, 3], expected: [3, 2] }, diff --git a/test/unit/builtins/loading.spec.ts b/test/unit/builtins/loading.spec.ts index fa213d03f..a0db5c16e 100644 --- a/test/unit/builtins/loading.spec.ts +++ b/test/unit/builtins/loading.spec.ts @@ -4,14 +4,14 @@ import * as util from "../../util"; describe("luaLibImport", () => { test("inline", () => { - util.testExpression`[0].push(1)` + util.testExpression`[0].indexOf(1)` .setOptions({ luaLibImport: tstl.LuaLibImportKind.Inline }) .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain('require("lualib_bundle")')) .expectToMatchJsResult(); }); test("require", () => { - util.testExpression`[0].push(1)` + util.testExpression`[0].indexOf(1)` .setOptions({ luaLibImport: tstl.LuaLibImportKind.Require }) .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain('require("lualib_bundle")')) .expectToMatchJsResult(); From cb327f3e427c815adb5efa46cf65b1f117844fe3 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:24:09 -0700 Subject: [PATCH 23/51] Replace array.shift with table.remove --- src/lualib/ArrayShift.ts | 3 --- src/transformation/builtins/array.ts | 6 +++++- 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 src/lualib/ArrayShift.ts diff --git a/src/lualib/ArrayShift.ts b/src/lualib/ArrayShift.ts deleted file mode 100644 index e39d9616b..000000000 --- a/src/lualib/ArrayShift.ts +++ /dev/null @@ -1,3 +0,0 @@ -function __TS__ArrayShift(this: void, arr: T[]): T { - return table.remove(arr, 1); -} diff --git a/src/transformation/builtins/array.ts b/src/transformation/builtins/array.ts index ca986fdf1..6b816bf33 100644 --- a/src/transformation/builtins/array.ts +++ b/src/transformation/builtins/array.ts @@ -89,7 +89,11 @@ export function transformArrayPrototypeCall( case "reverse": return transformLuaLibFunction(context, LuaLibFeature.ArrayReverse, node, caller); case "shift": - return transformLuaLibFunction(context, LuaLibFeature.ArrayShift, node, caller); + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("remove")), + [caller, lua.createNumericLiteral(1)], + node + ); case "unshift": return transformLuaLibFunction( context, From 7739e6af4dd540d975df1d8e5a6f25db54aa8441 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:29:50 -0700 Subject: [PATCH 24/51] Optimize array.join String manipulation is slow in lua, and one table.concat operation is much faster than many string concatenations --- src/lualib/ArrayJoin.ts | 11 +++++------ src/transformation/builtins/array.ts | 5 ++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/lualib/ArrayJoin.ts b/src/lualib/ArrayJoin.ts index 33e220181..f615e2e0c 100644 --- a/src/lualib/ArrayJoin.ts +++ b/src/lualib/ArrayJoin.ts @@ -1,8 +1,7 @@ -function __TS__ArrayJoin(this: unknown[], separator = ",") { - let result = ""; - for (const [index, value] of ipairs(this)) { - if (index > 1) result += separator; - result += value.toString(); +function __TS__ArrayJoin(this: void, arr: any[], separator = ",") { + const parts: string[] = []; + for (const i of $range(1, arr.length)) { + parts[i - 1] = arr[i - 1].toString(); } - return result; + return table.concat(parts, separator); } diff --git a/src/transformation/builtins/array.ts b/src/transformation/builtins/array.ts index 6b816bf33..84fea4daf 100644 --- a/src/transformation/builtins/array.ts +++ b/src/transformation/builtins/array.ts @@ -141,11 +141,14 @@ export function transformArrayPrototypeCall( const elementType = context.checker.getElementTypeOfArrayType(callerType); if (elementType && (isStringType(context, elementType) || isNumberType(context, elementType))) { const defaultSeparatorLiteral = lua.createStringLiteral(","); + const param = params[0]; const parameters = [ caller, node.arguments.length === 0 ? defaultSeparatorLiteral - : lua.createBinaryExpression(params[0], defaultSeparatorLiteral, lua.SyntaxKind.OrOperator), + : lua.isStringLiteral(param) + ? param + : lua.createBinaryExpression(param, defaultSeparatorLiteral, lua.SyntaxKind.OrOperator), ]; return lua.createCallExpression( From b6a1acb7002da816d89768998ddea2b143d8614b Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:42:54 -0700 Subject: [PATCH 25/51] Optimize array.concat --- src/lualib/ArrayConcat.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/lualib/ArrayConcat.ts b/src/lualib/ArrayConcat.ts index 30da83514..ed834b07c 100644 --- a/src/lualib/ArrayConcat.ts +++ b/src/lualib/ArrayConcat.ts @@ -1,18 +1,22 @@ -function __TS__ArrayConcat(this: void, arr1: any[], ...args: any[]): any[] { - const out: any[] = []; - for (const val of arr1) { - out[out.length] = val; +function __TS__ArrayConcat(this: void, arr: any[], items: any[]): any[] { + const result: any[] = []; + let len = 0; + for (const i of $range(1, arr.length)) { + len++; + result[len - 1] = arr[i - 1]; } - for (const arg of args) { - if (Array.isArray(arg)) { - const argAsArray = arg; - for (const val of argAsArray) { - out[out.length] = val; + for (const i of $range(1, items.length)) { + const item = items[i - 1]; + if (Array.isArray(item)) { + for (const j of $range(1, item.length)) { + len++; + result[len - 1] = item[j - 1]; } } else { - out[out.length] = arg; + len++; + result[len - 1] = item; } } - return out; + return result; } From e6310a1ec2e90d89b97a69159a9634ef4bd16974 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:43:17 -0700 Subject: [PATCH 26/51] Optimize array.every --- src/lualib/ArrayEvery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lualib/ArrayEvery.ts b/src/lualib/ArrayEvery.ts index 011dd9abe..757660513 100644 --- a/src/lualib/ArrayEvery.ts +++ b/src/lualib/ArrayEvery.ts @@ -3,8 +3,8 @@ function __TS__ArrayEvery( arr: T[], callbackfn: (value: T, index?: number, array?: any[]) => boolean ): boolean { - for (let i = 0; i < arr.length; i++) { - if (!callbackfn(arr[i], i, arr)) { + for (const i of $range(1, arr.length)) { + if (!callbackfn(arr[i - 1], i - 1, arr)) { return false; } } From 2f04d39801db175994615f7f08073bf333e930a9 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:43:40 -0700 Subject: [PATCH 27/51] Optimize array.filter --- src/lualib/ArrayFilter.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lualib/ArrayFilter.ts b/src/lualib/ArrayFilter.ts index 3d88ddffa..fb74d2af9 100644 --- a/src/lualib/ArrayFilter.ts +++ b/src/lualib/ArrayFilter.ts @@ -4,9 +4,11 @@ function __TS__ArrayFilter( callbackfn: (value: T, index?: number, array?: any[]) => boolean ): T[] { const result: T[] = []; - for (let i = 0; i < arr.length; i++) { - if (callbackfn(arr[i], i, arr)) { - result[result.length] = arr[i]; + let len = 0; + for (const i of $range(1, arr.length)) { + if (callbackfn(arr[i - 1], i - 1, arr)) { + len++; + result[len - 1] = arr[i - 1]; } } return result; From cb3887d552cd42fdd15f7a6ff161d6c4881d6d7e Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:43:59 -0700 Subject: [PATCH 28/51] Optimize array.find --- src/lualib/ArrayFind.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lualib/ArrayFind.ts b/src/lualib/ArrayFind.ts index 608bd3ef6..5f4321c4b 100644 --- a/src/lualib/ArrayFind.ts +++ b/src/lualib/ArrayFind.ts @@ -4,14 +4,11 @@ function __TS__ArrayFind( arr: T[], predicate: (value: T, index: number, obj: T[]) => unknown ): T | undefined { - const len = arr.length; - let k = 0; - while (k < len) { - const elem = arr[k]; - if (predicate(elem, k, arr)) { + for (const i of $range(1, arr.length)) { + const elem = arr[i - 1]; + if (predicate(elem, i - 1, arr)) { return elem; } - k += 1; } return undefined; From 5d88a1f60c1bf39bf1e6dfc62633b7a9f3cb699e Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:45:01 -0700 Subject: [PATCH 29/51] Optimize array.findIndex --- src/lualib/ArrayFindIndex.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lualib/ArrayFindIndex.ts b/src/lualib/ArrayFindIndex.ts index 53d83cb39..d185b17fc 100644 --- a/src/lualib/ArrayFindIndex.ts +++ b/src/lualib/ArrayFindIndex.ts @@ -3,9 +3,9 @@ function __TS__ArrayFindIndex( arr: T[], callbackFn: (element: T, index?: number, array?: T[]) => boolean ): number { - for (let i = 0, len = arr.length; i < len; i++) { - if (callbackFn(arr[i], i, arr)) { - return i; + for (const i of $range(1, arr.length)) { + if (callbackFn(arr[i - 1], i - 1, arr)) { + return i - 1; } } return -1; From 9504f6f95601309072644bf30882577636c4abf7 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:48:31 -0700 Subject: [PATCH 30/51] Optimize array.flat --- src/lualib/ArrayFlat.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/lualib/ArrayFlat.ts b/src/lualib/ArrayFlat.ts index 2bed314cb..4a5774f89 100644 --- a/src/lualib/ArrayFlat.ts +++ b/src/lualib/ArrayFlat.ts @@ -1,10 +1,23 @@ function __TS__ArrayFlat(this: void, array: any[], depth = 1): any[] { - let result: any[] = []; - for (const value of array) { + const result: any[] = []; + let len = 0; + for (const i of $range(1, array.length)) { + const value = array[i - 1]; if (depth > 0 && Array.isArray(value)) { - result = result.concat(__TS__ArrayFlat(value, depth - 1)); + let toAdd: any[]; + if (depth === 1) { + toAdd = value; + } else { + toAdd = value.flat(depth - 1); + } + for (const j of $range(1, toAdd.length)) { + const val = toAdd[j - 1]; + len++; + result[len - 1] = val; + } } else { - result[result.length] = value; + len++; + result[len - 1] = value; } } From 6ec0d7fe091f202165a6040526df4a7c7964a9aa Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:48:41 -0700 Subject: [PATCH 31/51] Optimize array.flat --- src/lualib/ArrayFlatMap.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lualib/ArrayFlatMap.ts b/src/lualib/ArrayFlatMap.ts index c31af9046..ddbd63206 100644 --- a/src/lualib/ArrayFlatMap.ts +++ b/src/lualib/ArrayFlatMap.ts @@ -3,13 +3,18 @@ function __TS__ArrayFlatMap( array: T[], callback: (value: T, index: number, array: T[]) => U | readonly U[] ): U[] { - let result: U[] = []; - for (let i = 0; i < array.length; i++) { - const value = callback(array[i], i, array); - if (type(value) === "table" && Array.isArray(value)) { - result = result.concat(value); + const result: U[] = []; + let len = 1; + for (const i of $range(1, array.length)) { + const value = callback(array[i - 1], i - 1, array); + if (Array.isArray(value)) { + for (const j of $range(1, value.length)) { + len++; + result[len - 1] = value[j - 1]; + } } else { - result[result.length] = value as U; + len++; + result[len - 1] = value as U; } } From ce108b456fb17b04a22621963ababadcb8c86f76 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:48:53 -0700 Subject: [PATCH 32/51] Optimize array.flat --- src/lualib/ArrayForEach.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lualib/ArrayForEach.ts b/src/lualib/ArrayForEach.ts index 96ac4a806..2ef6e2d8f 100644 --- a/src/lualib/ArrayForEach.ts +++ b/src/lualib/ArrayForEach.ts @@ -3,7 +3,7 @@ function __TS__ArrayForEach( arr: T[], callbackFn: (value: T, index?: number, array?: any[]) => any ): void { - for (let i = 0; i < arr.length; i++) { - callbackFn(arr[i], i, arr); + for (const i of $range(1, arr.length)) { + callbackFn(arr[i - 1], i - 1, arr); } } From 26254df280c19bc9634b31bb6bb4506e1c2973cd Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:49:50 -0700 Subject: [PATCH 33/51] Optimize array.includes --- src/lualib/ArrayIncludes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lualib/ArrayIncludes.ts b/src/lualib/ArrayIncludes.ts index b012f87bf..e40b9352f 100644 --- a/src/lualib/ArrayIncludes.ts +++ b/src/lualib/ArrayIncludes.ts @@ -11,8 +11,8 @@ function __TS__ArrayIncludes(this: T[], searchElement: T, fromIndex = 0): boo k = 0; } - for (const i of $range(k, len)) { - if (this[i] === searchElement) { + for (const i of $range(k + 1, len)) { + if (this[i - 1] === searchElement) { return true; } } From 56ac6bdd6cf36d6bcfff4beeaa5c0e182fa386fc Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:52:13 -0700 Subject: [PATCH 34/51] Optimize array.indexOf --- src/lualib/ArrayIndexOf.ts | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/lualib/ArrayIndexOf.ts b/src/lualib/ArrayIndexOf.ts index e91a1a5dc..ec7548635 100644 --- a/src/lualib/ArrayIndexOf.ts +++ b/src/lualib/ArrayIndexOf.ts @@ -1,31 +1,23 @@ -function __TS__ArrayIndexOf(this: void, arr: T[], searchElement: T, fromIndex?: number): number { +function __TS__ArrayIndexOf(this: void, arr: T[], searchElement: T, fromIndex = 0): number { const len = arr.length; if (len === 0) { return -1; } - let n = 0; - if (fromIndex) { - n = fromIndex; - } - - if (n >= len) { + if (fromIndex >= len) { return -1; } - let k: number; - if (n >= 0) { - k = n; - } else { - k = len + n; - if (k < 0) { - k = 0; + if (fromIndex < 0) { + fromIndex = len + fromIndex; + if (fromIndex < 0) { + fromIndex = 0; } } - for (let i = k; i < len; i++) { - if (arr[i] === searchElement) { - return i; + for (const i of $range(fromIndex + 1, len)) { + if (arr[i - 1] === searchElement) { + return i - 1; } } From 6395ffd59fe3a30f11ae0df5ef011e24988cb3f3 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:53:00 -0700 Subject: [PATCH 35/51] Optimize array.map --- src/lualib/ArrayMap.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lualib/ArrayMap.ts b/src/lualib/ArrayMap.ts index 04ee22a48..0af91db35 100644 --- a/src/lualib/ArrayMap.ts +++ b/src/lualib/ArrayMap.ts @@ -1,7 +1,7 @@ function __TS__ArrayMap(this: void, arr: T[], callbackfn: (value: T, index?: number, array?: T[]) => U): U[] { - const newArray: U[] = []; - for (let i = 0; i < arr.length; i++) { - newArray[i] = callbackfn(arr[i], i, arr); + const result: U[] = []; + for (const i of $range(1, arr.length)) { + result[i - 1] = callbackfn(arr[i - 1], i - 1, arr); } - return newArray; + return result; } From 1b505f69cbfd7abfbf3c6771f5c86278893063f4 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:53:33 -0700 Subject: [PATCH 36/51] Optimize array.reduce and array.reduceRight --- src/lualib/ArrayReduce.ts | 6 +++--- src/lualib/ArrayReduceRight.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lualib/ArrayReduce.ts b/src/lualib/ArrayReduce.ts index 5ac7bd856..bfcafc080 100644 --- a/src/lualib/ArrayReduce.ts +++ b/src/lualib/ArrayReduce.ts @@ -12,7 +12,7 @@ function __TS__ArrayReduce( // Check if initial value is present in function call if (select("#", ...initial) !== 0) { - [accumulator] = select(1, ...initial); + [accumulator] = [...initial]; } else if (len > 0) { accumulator = arr[0] as unknown as TAccumulator; k = 1; @@ -20,8 +20,8 @@ function __TS__ArrayReduce( throw "Reduce of empty array with no initial value"; } - for (const i of $range(k, len - 1)) { - accumulator = callbackFn(accumulator, arr[i], i, arr); + for (const i of $range(k + 1, len)) { + accumulator = callbackFn(accumulator, arr[i - 1], i - 1, arr); } return accumulator; diff --git a/src/lualib/ArrayReduceRight.ts b/src/lualib/ArrayReduceRight.ts index 26258b862..7104973f3 100644 --- a/src/lualib/ArrayReduceRight.ts +++ b/src/lualib/ArrayReduceRight.ts @@ -12,7 +12,7 @@ function __TS__ArrayReduceRight( // Check if initial value is present in function call if (select("#", ...initial) !== 0) { - [accumulator] = select(1, ...initial); + [accumulator] = [...initial]; } else if (len > 0) { accumulator = arr[k] as unknown as TAccumulator; k -= 1; @@ -20,8 +20,8 @@ function __TS__ArrayReduceRight( throw "Reduce of empty array with no initial value"; } - for (const i of $range(k, 0, -1)) { - accumulator = callbackFn(accumulator, arr[i], i, arr); + for (const i of $range(k + 1, 1, -1)) { + accumulator = callbackFn(accumulator, arr[i - 1], i - 1, arr); } return accumulator; From d395374fb2c33c8273e6b5634c0a477ea3b1f75e Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:54:09 -0700 Subject: [PATCH 37/51] Optimize array.reverse --- src/lualib/ArrayReverse.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lualib/ArrayReverse.ts b/src/lualib/ArrayReverse.ts index b4a97a2c1..6a3f81fd5 100644 --- a/src/lualib/ArrayReverse.ts +++ b/src/lualib/ArrayReverse.ts @@ -1,12 +1,12 @@ function __TS__ArrayReverse(this: void, arr: any[]): any[] { - let i = 0; - let j = arr.length - 1; + let i = 1; + let j = arr.length; while (i < j) { - const temp = arr[j]; - arr[j] = arr[i]; - arr[i] = temp; - i += 1; - j -= 1; + const temp = arr[j - 1]; + arr[j - 1] = arr[i - 1]; + arr[i - 1] = temp; + i++; + j--; } return arr; } From 98d601bf9cc4866f8afedc9729e3e2649aa41bf8 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:55:07 -0700 Subject: [PATCH 38/51] Optimize array.setLength --- src/lualib/ArraySetLength.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lualib/ArraySetLength.ts b/src/lualib/ArraySetLength.ts index e1bd8ea03..abf4def0c 100644 --- a/src/lualib/ArraySetLength.ts +++ b/src/lualib/ArraySetLength.ts @@ -8,8 +8,8 @@ function __TS__ArraySetLength(this: void, arr: T[], length: number): number { // non-integer throw `invalid array length: ${length}`; } - for (let i = arr.length - 1; i >= length; --i) { - arr[i] = undefined; + for (const i of $range(length + 1, arr.length)) { + arr[i - 1] = undefined; } return length; } From ee41dd709466233067fd19a3ecdf9403a81501c1 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:56:32 -0700 Subject: [PATCH 39/51] Optimize array.slice --- src/lualib/ArraySlice.ts | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/lualib/ArraySlice.ts b/src/lualib/ArraySlice.ts index d38d96006..c9b97d044 100644 --- a/src/lualib/ArraySlice.ts +++ b/src/lualib/ArraySlice.ts @@ -1,34 +1,39 @@ // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.slice -function __TS__ArraySlice(this: void, list: T[], first: number, last: number): T[] { +function __TS__ArraySlice(this: void, list: T[], first?: number, last?: number): T[] { const len = list.length; - const relativeStart = first || 0; - - let k: number; - if (relativeStart < 0) { - k = Math.max(len + relativeStart, 0); + first = first ?? 0; + if (first < 0) { + first = len + first; + if (first < 0) { + first = 0; + } } else { - k = Math.min(relativeStart, len); - } - - let relativeEnd = last; - if (last === undefined) { - relativeEnd = len; + if (first > len) { + first = len; + } } - let final: number; - if (relativeEnd < 0) { - final = Math.max(len + relativeEnd, 0); + last = last ?? len; + if (last < 0) { + last = len + last; + if (last < 0) { + last = 0; + } } else { - final = Math.min(relativeEnd, len); + if (last > len) { + last = len; + } } const out = []; - let n = 0; - while (k < final) { - out[n] = list[k]; - k++; + first++; + last++; + let n = 1; + while (first < last) { + out[n - 1] = list[first - 1]; + first++; n++; } return out; From 017592fd17fb89645b9f48e6394386f3b66122f0 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:56:53 -0700 Subject: [PATCH 40/51] Optimize array.some --- src/lualib/ArraySome.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lualib/ArraySome.ts b/src/lualib/ArraySome.ts index 9122b1bbc..a3165bbde 100644 --- a/src/lualib/ArraySome.ts +++ b/src/lualib/ArraySome.ts @@ -3,8 +3,8 @@ function __TS__ArraySome( arr: T[], callbackfn: (value: T, index?: number, array?: any[]) => boolean ): boolean { - for (let i = 0; i < arr.length; i++) { - if (callbackfn(arr[i], i, arr)) { + for (const i of $range(1, arr.length)) { + if (callbackfn(arr[i - 1], i - 1, arr)) { return true; } } From 743b5d96768bea3584ad50936c6ad1b60064dfa3 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:57:43 -0700 Subject: [PATCH 41/51] Optimize array.splice --- src/lualib/ArraySplice.ts | 74 ++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/src/lualib/ArraySplice.ts b/src/lualib/ArraySplice.ts index d4a122181..ee7c20d11 100644 --- a/src/lualib/ArraySplice.ts +++ b/src/lualib/ArraySplice.ts @@ -1,20 +1,24 @@ // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.splice -function __TS__ArraySplice(this: void, list: T[], ...args: T[]): T[] { - const len = list.length; +function __TS__ArraySplice(this: void, arr: T[], ...args: any[]): T[] { + const len = arr.length; const actualArgumentCount = select("#", ...args); - const start = select(1, ...args)[0] as unknown as number; - const deleteCount = select(2, ...args)[0] as unknown as number; - - let actualStart: number; + let start = args[0] as number; + const deleteCount = args[1] as number; if (start < 0) { - actualStart = Math.max(len + start, 0); - } else { - actualStart = Math.min(start, len); + start = len + start; + if (start < 0) { + start = 0; + } + } else if (start > len) { + start = len; } - const itemCount = Math.max(actualArgumentCount - 2, 0); + let itemCount = actualArgumentCount - 2; + if (itemCount < 0) { + itemCount = 0; + } let actualDeleteCount: number; @@ -23,56 +27,62 @@ function __TS__ArraySplice(this: void, list: T[], ...args: T[]): T[] { actualDeleteCount = 0; } else if (actualArgumentCount === 1) { // ECMA-spec line 6: if number of actual arguments is 1 - actualDeleteCount = len - actualStart; + actualDeleteCount = len - start; } else { - actualDeleteCount = Math.min(Math.max(deleteCount || 0, 0), len - actualStart); + actualDeleteCount = deleteCount || 0; + if (actualDeleteCount < 0) { + actualDeleteCount = 0; + } + if (actualDeleteCount > len - start) { + actualDeleteCount = len - start; + } } const out: T[] = []; - for (let k = 0; k < actualDeleteCount; k++) { - const from = actualStart + k; + for (const k of $range(1, actualDeleteCount)) { + const from = start + k; - if (list[from]) { - out[k] = list[from]; + if (arr[from - 1] !== undefined) { + out[k - 1] = arr[from - 1]; } } if (itemCount < actualDeleteCount) { - for (let k = actualStart; k < len - actualDeleteCount; k++) { + for (const k of $range(start + 1, len - actualDeleteCount)) { const from = k + actualDeleteCount; const to = k + itemCount; - if (list[from]) { - list[to] = list[from]; + if (arr[from - 1]) { + arr[to - 1] = arr[from - 1]; } else { - list[to] = undefined; + arr[to - 1] = undefined; } } - for (let k = len; k > len - actualDeleteCount + itemCount; k--) { - list[k - 1] = undefined; + for (const k of $range(len - actualDeleteCount + itemCount + 1, len)) { + arr[k - 1] = undefined; } } else if (itemCount > actualDeleteCount) { - for (let k = len - actualDeleteCount; k > actualStart; k--) { - const from = k + actualDeleteCount - 1; - const to = k + itemCount - 1; + for (const k of $range(len - actualDeleteCount, start + 1, -1)) { + const from = k + actualDeleteCount; + const to = k + itemCount; - if (list[from]) { - list[to] = list[from]; + if (arr[from - 1]) { + arr[to - 1] = arr[from - 1]; } else { - list[to] = undefined; + arr[to - 1] = undefined; } } } - let j = actualStart; + let j = start + 1; for (const i of $range(3, actualArgumentCount)) { - list[j] = select(i, ...args)[0]; + arr[j - 1] = args[i - 1]; j++; } - for (let k = list.length - 1; k >= len - actualDeleteCount + itemCount; k--) { - list[k] = undefined; + for (const k of $range(arr.length, len - actualDeleteCount + itemCount + 1, -1)) { + arr[k - 1] = undefined; } return out; From be7a2782b5fa2f95490910a00f41651c6221fb8e Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:58:07 -0700 Subject: [PATCH 42/51] Optimize array.toObject --- src/lualib/ArrayToObject.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lualib/ArrayToObject.ts b/src/lualib/ArrayToObject.ts index 460a77250..51f91b9c2 100644 --- a/src/lualib/ArrayToObject.ts +++ b/src/lualib/ArrayToObject.ts @@ -1,7 +1,7 @@ function __TS__ArrayToObject(this: void, array: T[]): Record { const object: Record = {}; - for (let i = 0; i < array.length; i += 1) { - object[i] = array[i]; + for (const i of $range(1, array.length)) { + object[i - 1] = array[i - 1]; } return object; } From d45bf6d7451c389c891c5f4faf5c73c7fd5eb72c Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:58:40 -0700 Subject: [PATCH 43/51] Optimize array.unshift --- src/lualib/ArrayUnshift.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lualib/ArrayUnshift.ts b/src/lualib/ArrayUnshift.ts index 95dc15b8f..84b9cacda 100644 --- a/src/lualib/ArrayUnshift.ts +++ b/src/lualib/ArrayUnshift.ts @@ -1,6 +1,11 @@ -function __TS__ArrayUnshift(this: void, arr: T[], ...items: T[]): number { - for (let i = items.length - 1; i >= 0; --i) { - table.insert(arr, 1, items[i]); +function __TS__ArrayUnshift(this: void, arr: T[], items: T[]): number { + const length = items.length; + + for (const i of $range(arr.length, 1, -1)) { + arr[i + length - 1] = arr[i - 1]; + } + for (const i of $range(1, length)) { + arr[i - 1] = items[i - 1]; } return arr.length; } From 45ed7448bf37cb4d0a99d9b9f079894580f9d6d6 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 18:44:32 -0700 Subject: [PATCH 44/51] Remove ArrayShift from LuaLibFeature enum --- src/LuaLib.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 03483d2cc..ef77f9c40 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -18,7 +18,6 @@ export enum LuaLibFeature { ArrayReduce = "ArrayReduce", ArrayReduceRight = "ArrayReduceRight", ArrayReverse = "ArrayReverse", - ArrayShift = "ArrayShift", ArrayUnshift = "ArrayUnshift", ArraySort = "ArraySort", ArraySlice = "ArraySlice", From 8880cde097d052f9797cab4f9874be6f0138c4bf Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 21:22:12 -0700 Subject: [PATCH 45/51] Fix array.flatMap --- src/lualib/ArrayFlatMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lualib/ArrayFlatMap.ts b/src/lualib/ArrayFlatMap.ts index ddbd63206..ec11641aa 100644 --- a/src/lualib/ArrayFlatMap.ts +++ b/src/lualib/ArrayFlatMap.ts @@ -4,7 +4,7 @@ function __TS__ArrayFlatMap( callback: (value: T, index: number, array: T[]) => U | readonly U[] ): U[] { const result: U[] = []; - let len = 1; + let len = 0; for (const i of $range(1, array.length)) { const value = callback(array[i - 1], i - 1, array); if (Array.isArray(value)) { From db04b7717882bd7420342920c87c85c4a4782dfe Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Tue, 7 Sep 2021 21:23:09 -0700 Subject: [PATCH 46/51] Add thisArg to all array functions --- src/lualib/ArrayEvery.ts | 5 +++-- src/lualib/ArrayFilter.ts | 5 +++-- src/lualib/ArrayFind.ts | 5 +++-- src/lualib/ArrayFindIndex.ts | 5 +++-- src/lualib/ArrayFlatMap.ts | 5 +++-- src/lualib/ArrayForEach.ts | 5 +++-- src/lualib/ArrayMap.ts | 9 +++++++-- src/lualib/ArraySome.ts | 5 +++-- 8 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/lualib/ArrayEvery.ts b/src/lualib/ArrayEvery.ts index 757660513..ce2e68f8c 100644 --- a/src/lualib/ArrayEvery.ts +++ b/src/lualib/ArrayEvery.ts @@ -1,10 +1,11 @@ function __TS__ArrayEvery( this: void, arr: T[], - callbackfn: (value: T, index?: number, array?: any[]) => boolean + callbackfn: (value: T, index?: number, array?: any[]) => boolean, + thisArg?: any ): boolean { for (const i of $range(1, arr.length)) { - if (!callbackfn(arr[i - 1], i - 1, arr)) { + if (!callbackfn.call(thisArg, arr[i - 1], i - 1, arr)) { return false; } } diff --git a/src/lualib/ArrayFilter.ts b/src/lualib/ArrayFilter.ts index fb74d2af9..2e5752ab9 100644 --- a/src/lualib/ArrayFilter.ts +++ b/src/lualib/ArrayFilter.ts @@ -1,12 +1,13 @@ function __TS__ArrayFilter( this: void, arr: T[], - callbackfn: (value: T, index?: number, array?: any[]) => boolean + callbackfn: (value: T, index?: number, array?: any[]) => boolean, + thisArg?: any ): T[] { const result: T[] = []; let len = 0; for (const i of $range(1, arr.length)) { - if (callbackfn(arr[i - 1], i - 1, arr)) { + if (callbackfn.call(thisArg, arr[i - 1], i - 1, arr)) { len++; result[len - 1] = arr[i - 1]; } diff --git a/src/lualib/ArrayFind.ts b/src/lualib/ArrayFind.ts index 5f4321c4b..0977851b4 100644 --- a/src/lualib/ArrayFind.ts +++ b/src/lualib/ArrayFind.ts @@ -2,11 +2,12 @@ function __TS__ArrayFind( this: void, arr: T[], - predicate: (value: T, index: number, obj: T[]) => unknown + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any ): T | undefined { for (const i of $range(1, arr.length)) { const elem = arr[i - 1]; - if (predicate(elem, i - 1, arr)) { + if (predicate.call(thisArg, elem, i - 1, arr)) { return elem; } } diff --git a/src/lualib/ArrayFindIndex.ts b/src/lualib/ArrayFindIndex.ts index d185b17fc..c5cdf5296 100644 --- a/src/lualib/ArrayFindIndex.ts +++ b/src/lualib/ArrayFindIndex.ts @@ -1,10 +1,11 @@ function __TS__ArrayFindIndex( this: void, arr: T[], - callbackFn: (element: T, index?: number, array?: T[]) => boolean + callbackFn: (element: T, index?: number, array?: T[]) => boolean, + thisArg?: any ): number { for (const i of $range(1, arr.length)) { - if (callbackFn(arr[i - 1], i - 1, arr)) { + if (callbackFn.call(thisArg, arr[i - 1], i - 1, arr)) { return i - 1; } } diff --git a/src/lualib/ArrayFlatMap.ts b/src/lualib/ArrayFlatMap.ts index ec11641aa..3850eb535 100644 --- a/src/lualib/ArrayFlatMap.ts +++ b/src/lualib/ArrayFlatMap.ts @@ -1,12 +1,13 @@ function __TS__ArrayFlatMap( this: void, array: T[], - callback: (value: T, index: number, array: T[]) => U | readonly U[] + callback: (value: T, index: number, array: T[]) => U | readonly U[], + thisArg?: any ): U[] { const result: U[] = []; let len = 0; for (const i of $range(1, array.length)) { - const value = callback(array[i - 1], i - 1, array); + const value = callback.call(thisArg, array[i - 1], i - 1, array); if (Array.isArray(value)) { for (const j of $range(1, value.length)) { len++; diff --git a/src/lualib/ArrayForEach.ts b/src/lualib/ArrayForEach.ts index 2ef6e2d8f..52fa572f7 100644 --- a/src/lualib/ArrayForEach.ts +++ b/src/lualib/ArrayForEach.ts @@ -1,9 +1,10 @@ function __TS__ArrayForEach( this: void, arr: T[], - callbackFn: (value: T, index?: number, array?: any[]) => any + callbackFn: (value: T, index?: number, array?: any[]) => any, + thisArg?: any ): void { for (const i of $range(1, arr.length)) { - callbackFn(arr[i - 1], i - 1, arr); + callbackFn.call(thisArg, arr[i - 1], i - 1, arr); } } diff --git a/src/lualib/ArrayMap.ts b/src/lualib/ArrayMap.ts index 0af91db35..3382b464e 100644 --- a/src/lualib/ArrayMap.ts +++ b/src/lualib/ArrayMap.ts @@ -1,7 +1,12 @@ -function __TS__ArrayMap(this: void, arr: T[], callbackfn: (value: T, index?: number, array?: T[]) => U): U[] { +function __TS__ArrayMap( + this: void, + arr: T[], + callbackfn: (value: T, index?: number, array?: T[]) => U, + thisArg?: any +): U[] { const result: U[] = []; for (const i of $range(1, arr.length)) { - result[i - 1] = callbackfn(arr[i - 1], i - 1, arr); + result[i - 1] = callbackfn.call(thisArg, arr[i - 1], i - 1, arr); } return result; } diff --git a/src/lualib/ArraySome.ts b/src/lualib/ArraySome.ts index a3165bbde..5253b2c57 100644 --- a/src/lualib/ArraySome.ts +++ b/src/lualib/ArraySome.ts @@ -1,10 +1,11 @@ function __TS__ArraySome( this: void, arr: T[], - callbackfn: (value: T, index?: number, array?: any[]) => boolean + callbackfn: (value: T, index?: number, array?: any[]) => boolean, + thisArg?: any ): boolean { for (const i of $range(1, arr.length)) { - if (callbackfn(arr[i - 1], i - 1, arr)) { + if (callbackfn.call(thisArg, arr[i - 1], i - 1, arr)) { return true; } } From 535bf1ee5c1d11461572945e5ad028a83a50ae1d Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Thu, 9 Sep 2021 16:35:53 -0700 Subject: [PATCH 47/51] Fix formatting --- src/transformation/visitors/literal.ts | 91 +++++++++++++------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 427bd28c0..812377a08 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -71,13 +71,14 @@ const transformObjectLiteralExpression: FunctionVisitor 0) { - lastPrecedingStatementsIndex = i; - } + const propertyPrecedingStatements = context.popPrecedingStatements(); + precedingStatements.push(propertyPrecedingStatements); + if (propertyPrecedingStatements.length > 0) { + lastPrecedingStatementsIndex = i; } + } - // Expressions referenced before others that produced preceding statements need to be cached in temps - if (lastPrecedingStatementsIndex >= 0) { - for (let i = 0; i < properties.length; ++i) { - const property = properties[i]; - - // Bubble up preceding statements - const propertyPrecedingStatements = precedingStatements[i]; - context.addPrecedingStatements(propertyPrecedingStatements); - - // Ignore expressions after the last one the generated preceding statements - if (i >= lastPrecedingStatementsIndex) continue; - - if (lua.isTableFieldExpression(property)) { - // Skip fields whose values are: - // - literal values that couldn't be affected by preceding statements - // - temp identifiers which are results from preceding statements - if ( - !lua.isLiteral(property.value) && - !(propertyPrecedingStatements.length > 0 && lua.isIdentifier(property.value)) - ) { - property.value = moveToPrecedingTemp(context, property.value); - } - } else { - properties[i] = moveToPrecedingTemp(context, property); + // Expressions referenced before others that produced preceding statements need to be cached in temps + if (lastPrecedingStatementsIndex >= 0) { + for (let i = 0; i < properties.length; ++i) { + const property = properties[i]; + + // Bubble up preceding statements + const propertyPrecedingStatements = precedingStatements[i]; + context.addPrecedingStatements(propertyPrecedingStatements); + + // Ignore expressions after the last one the generated preceding statements + if (i >= lastPrecedingStatementsIndex) continue; + + if (lua.isTableFieldExpression(property)) { + // Skip fields whose values are: + // - literal values that couldn't be affected by preceding statements + // - temp identifiers which are results from preceding statements + if ( + !lua.isLiteral(property.value) && + !(propertyPrecedingStatements.length > 0 && lua.isIdentifier(property.value)) + ) { + property.value = moveToPrecedingTemp(context, property.value); } + } else { + properties[i] = moveToPrecedingTemp(context, property); } } + } - // Sort into field expressions and tables to pass into __TS__ObjectAssign - let fields: lua.TableFieldExpression[] = []; - const tableExpressions: lua.Expression[] = []; - for (const property of properties) { - if (lua.isTableFieldExpression(property)) { - fields.push(property); - } else { - if (fields.length > 0) { - tableExpressions.push(lua.createTableExpression(fields)); - } - tableExpressions.push(property); - fields = []; + // Sort into field expressions and tables to pass into __TS__ObjectAssign + let fields: lua.TableFieldExpression[] = []; + const tableExpressions: lua.Expression[] = []; + for (const property of properties) { + if (lua.isTableFieldExpression(property)) { + fields.push(property); + } else { + if (fields.length > 0) { + tableExpressions.push(lua.createTableExpression(fields)); } + tableExpressions.push(property); + fields = []; } + } if (tableExpressions.length === 0) { return lua.createTableExpression(fields, expression); From dbffcbc3d608bf94c89d6e7959c3a4d17c41836f Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Thu, 9 Sep 2021 16:45:36 -0700 Subject: [PATCH 48/51] Another optimization for array unshift --- src/lualib/ArrayUnshift.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lualib/ArrayUnshift.ts b/src/lualib/ArrayUnshift.ts index 84b9cacda..2b3414918 100644 --- a/src/lualib/ArrayUnshift.ts +++ b/src/lualib/ArrayUnshift.ts @@ -1,5 +1,6 @@ function __TS__ArrayUnshift(this: void, arr: T[], items: T[]): number { const length = items.length; + if (length === 0) return arr.length; for (const i of $range(arr.length, 1, -1)) { arr[i + length - 1] = arr[i - 1]; From 73977fa329598ba09def964ad9bcbff06183e887 Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Thu, 9 Sep 2021 19:30:20 -0700 Subject: [PATCH 49/51] Optimize many other lualib --- src/LuaLib.ts | 1 - src/lualib/ArrayIsArray.ts | 4 +-- src/lualib/FunctionBind.ts | 4 +-- src/lualib/ObjectAssign.ts | 8 +++--- src/lualib/ObjectEntries.ts | 4 ++- src/lualib/ObjectKeys.ts | 4 ++- src/lualib/ObjectValues.ts | 4 ++- src/lualib/Spread.ts | 8 +++--- src/lualib/StringConcat.ts | 7 ----- src/lualib/StringReplaceAll.ts | 15 ++++++----- src/lualib/StringSplit.ts | 36 +++++++++++++------------- src/transformation/builtins/object.ts | 3 ++- src/transformation/builtins/string.ts | 8 ++++-- src/transformation/visitors/literal.ts | 8 +++++- 14 files changed, 63 insertions(+), 51 deletions(-) delete mode 100644 src/lualib/StringConcat.ts diff --git a/src/LuaLib.ts b/src/LuaLib.ts index ef77f9c40..00f5786c0 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -76,7 +76,6 @@ export enum LuaLibFeature { StringAccess = "StringAccess", StringCharAt = "StringCharAt", StringCharCodeAt = "StringCharCodeAt", - StringConcat = "StringConcat", StringEndsWith = "StringEndsWith", StringIncludes = "StringIncludes", StringPadEnd = "StringPadEnd", diff --git a/src/lualib/ArrayIsArray.ts b/src/lualib/ArrayIsArray.ts index 417ae6e6e..f324cb116 100644 --- a/src/lualib/ArrayIsArray.ts +++ b/src/lualib/ArrayIsArray.ts @@ -1,7 +1,7 @@ -declare type NextEmptyCheck = (this: void, table: any, index: undefined) => unknown | undefined; +declare type NextEmptyCheck = (this: void, table: any, index?: undefined) => unknown | undefined; function __TS__ArrayIsArray(this: void, value: any): value is any[] { // Workaround to determine if value is an array or not (fails in case of objects without keys) // See discussion in: https://github.com/TypeScriptToLua/TypeScriptToLua/pull/737 - return type(value) === "table" && (1 in value || (next as NextEmptyCheck)(value, undefined) === undefined); + return type(value) === "table" && (1 in value || (next as NextEmptyCheck)(value) === undefined); } diff --git a/src/lualib/FunctionBind.ts b/src/lualib/FunctionBind.ts index e12fbe37c..579290b68 100644 --- a/src/lualib/FunctionBind.ts +++ b/src/lualib/FunctionBind.ts @@ -5,9 +5,7 @@ function __TS__FunctionBind( ...boundArgs: any[] ): (...args: any[]) => any { return (...args: any[]) => { - for (let i = 0; i < boundArgs.length; ++i) { - table.insert(args, i + 1, boundArgs[i]); - } + __TS__ArrayUnshift(args, boundArgs); return fn(thisArg, ...args); }; } diff --git a/src/lualib/ObjectAssign.ts b/src/lualib/ObjectAssign.ts index bcf521c07..521ecd145 100644 --- a/src/lualib/ObjectAssign.ts +++ b/src/lualib/ObjectAssign.ts @@ -1,11 +1,13 @@ // https://tc39.github.io/ecma262/#sec-object.assign // eslint-disable-next-line @typescript-eslint/ban-types -function __TS__ObjectAssign(this: void, to: T, ...sources: object[]): T { +function __TS__ObjectAssign(this: void, sources: object[]): T { + const to = sources[0] as T; if (to === undefined) { - return to; + return; } - for (const source of sources) { + for (const i of $range(2, sources.length)) { + const source = sources[i - 1]; for (const key in source) { to[key] = source[key]; } diff --git a/src/lualib/ObjectEntries.ts b/src/lualib/ObjectEntries.ts index f491bd039..7ec829a06 100644 --- a/src/lualib/ObjectEntries.ts +++ b/src/lualib/ObjectEntries.ts @@ -1,7 +1,9 @@ function __TS__ObjectEntries(this: void, obj: any): Array { const result = []; + let len = 0; for (const key in obj) { - result[result.length] = [key, obj[key]]; + len++; + result[len - 1] = [key, obj[key]]; } return result; } diff --git a/src/lualib/ObjectKeys.ts b/src/lualib/ObjectKeys.ts index 8163a620a..ee458c557 100644 --- a/src/lualib/ObjectKeys.ts +++ b/src/lualib/ObjectKeys.ts @@ -1,7 +1,9 @@ function __TS__ObjectKeys(this: void, obj: any): Array { const result = []; + let len = 0; for (const key in obj) { - result[result.length] = key; + len++; + result[len - 1] = key; } return result; } diff --git a/src/lualib/ObjectValues.ts b/src/lualib/ObjectValues.ts index e850f6a31..746119533 100644 --- a/src/lualib/ObjectValues.ts +++ b/src/lualib/ObjectValues.ts @@ -1,7 +1,9 @@ function __TS__ObjectValues(this: void, obj: any): Array { const result = []; + let len = 0; for (const key in obj) { - result[result.length] = obj[key]; + len++; + result[len - 1] = obj[key]; } return result; } diff --git a/src/lualib/Spread.ts b/src/lualib/Spread.ts index 6bc3bf628..6ade7963f 100644 --- a/src/lualib/Spread.ts +++ b/src/lualib/Spread.ts @@ -2,12 +2,14 @@ function __TS__Spread(this: void, iterable: string | Iterable): LuaMultiRe const arr = []; if (typeof iterable === "string") { // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < iterable.length; i += 1) { - arr[arr.length] = iterable[i]; + for (const i of $range(0, iterable.length - 1)) { + arr[i] = iterable[i]; } } else { + let len = 0; for (const item of iterable) { - arr[arr.length] = item; + len++; + arr[len - 1] = item; } } return $multi(...arr); diff --git a/src/lualib/StringConcat.ts b/src/lualib/StringConcat.ts deleted file mode 100644 index 657ae5c24..000000000 --- a/src/lualib/StringConcat.ts +++ /dev/null @@ -1,7 +0,0 @@ -function __TS__StringConcat(this: void, str1: string, ...args: string[]): string { - let out = str1; - for (const arg of args) { - out += arg; - } - return out; -} diff --git a/src/lualib/StringReplaceAll.ts b/src/lualib/StringReplaceAll.ts index e288a1497..235fc9748 100644 --- a/src/lualib/StringReplaceAll.ts +++ b/src/lualib/StringReplaceAll.ts @@ -4,22 +4,23 @@ function __TS__StringReplaceAll( searchValue: string, replaceValue: string | ((match: string, offset: number, string: string) => string) ): string { - let replacer: (match: string, offset: number, string: string) => string; if (typeof replaceValue === "string") { - replacer = () => replaceValue; - } else { - replacer = replaceValue; + const concat = table.concat(source.split(searchValue), replaceValue); + if (searchValue.length === 0) { + return replaceValue + concat + replaceValue; + } + return concat; } const parts: string[] = []; let partsIndex = 1; const sub = string.sub; if (searchValue.length === 0) { - parts[0] = replacer("", 0, source); + parts[0] = replaceValue("", 0, source); partsIndex = 2; for (const i of $range(1, source.length)) { parts[partsIndex - 1] = sub(source, i, i); - parts[partsIndex] = replacer("", i, source); + parts[partsIndex] = replaceValue("", i, source); partsIndex += 2; } } else { @@ -29,7 +30,7 @@ function __TS__StringReplaceAll( const [startPos, endPos] = find(source, searchValue, currentPos, true); if (!startPos) break; parts[partsIndex - 1] = sub(source, currentPos, startPos - 1); - parts[partsIndex] = replacer(searchValue, startPos - 1, source); + parts[partsIndex] = replaceValue(searchValue, startPos - 1, source); partsIndex += 2; currentPos = endPos + 1; diff --git a/src/lualib/StringSplit.ts b/src/lualib/StringSplit.ts index 734dac38c..7b5565082 100644 --- a/src/lualib/StringSplit.ts +++ b/src/lualib/StringSplit.ts @@ -7,30 +7,30 @@ function __TS__StringSplit(this: void, source: string, separator?: string, limit return []; } - const out = []; - let index = 0; - let count = 0; + const result = []; + let resultIndex = 1; + const sub = string.sub; if (separator === undefined || separator === "") { - while (index < source.length - 1 && count < limit) { - out[count] = source[index]; - count++; - index++; + for (const i of $range(1, source.length)) { + result[resultIndex - 1] = sub(source, i, i); + resultIndex++; } } else { - const separatorLength = separator.length; - let nextIndex = source.indexOf(separator); - while (nextIndex >= 0 && count < limit) { - out[count] = source.substring(index, nextIndex); - count++; - index = nextIndex + separatorLength; - nextIndex = source.indexOf(separator, index); + const find = string.find; + let currentPos = 1; + while (resultIndex <= limit) { + const [startPos, endPos] = find(source, separator, currentPos, true); + if (!startPos) break; + result[resultIndex - 1] = sub(source, currentPos, startPos - 1); + resultIndex++; + currentPos = endPos + 1; } - } - if (count < limit) { - out[count] = source.substring(index); + if (resultIndex <= limit) { + result[resultIndex - 1] = sub(source, currentPos); + } } - return out; + return result; } diff --git a/src/transformation/builtins/object.ts b/src/transformation/builtins/object.ts index 2db793f91..bc89d7518 100644 --- a/src/transformation/builtins/object.ts +++ b/src/transformation/builtins/object.ts @@ -3,6 +3,7 @@ import { TransformationContext } from "../context"; import { unsupportedProperty } from "../utils/diagnostics"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { PropertyCallExpression, transformArguments } from "../visitors/call"; +import { wrapInReadonlyTable } from "../utils/lua-ast"; export function transformObjectConstructorCall( context: TransformationContext, @@ -14,7 +15,7 @@ export function transformObjectConstructorCall( switch (methodName) { case "assign": - return transformLuaLibFunction(context, LuaLibFeature.ObjectAssign, expression, ...args); + return transformLuaLibFunction(context, LuaLibFeature.ObjectAssign, expression, wrapInReadonlyTable(args)); case "defineProperty": return transformLuaLibFunction(context, LuaLibFeature.ObjectDefineProperty, expression, ...args); case "entries": diff --git a/src/transformation/builtins/string.ts b/src/transformation/builtins/string.ts index c71f855fc..d151d8d47 100644 --- a/src/transformation/builtins/string.ts +++ b/src/transformation/builtins/string.ts @@ -2,7 +2,7 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; import { unsupportedProperty } from "../utils/diagnostics"; -import { addToNumericExpression, createNaN, getNumberLiteralValue } from "../utils/lua-ast"; +import { addToNumericExpression, createNaN, getNumberLiteralValue, wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { PropertyCallExpression, transformArguments, transformCallAndArguments } from "../visitors/call"; @@ -30,7 +30,11 @@ export function transformStringPrototypeCall( case "replaceAll": return transformLuaLibFunction(context, LuaLibFeature.StringReplaceAll, node, caller, ...params); case "concat": - return transformLuaLibFunction(context, LuaLibFeature.StringConcat, node, caller, ...params); + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("concat")), + [wrapInTable(caller, ...params)], + node + ); case "indexOf": { const stringExpression = createStringCall( diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 812377a08..ec6573a01 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -11,6 +11,7 @@ import { isArrayType } from "../utils/typescript"; import { transformFunctionLikeDeclaration } from "./function"; import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; import { findMultiAssignmentViolations } from "./language-extensions/multi"; +import { wrapInReadonlyTable } from "../utils/lua-ast"; // TODO: Move to object-literal.ts? export function transformPropertyName(context: TransformationContext, node: ts.PropertyName): lua.Expression { @@ -174,7 +175,12 @@ const transformObjectLiteralExpression: FunctionVisitor Date: Thu, 9 Sep 2021 19:32:59 -0700 Subject: [PATCH 50/51] Update switch snapshots to use optimized array push --- test/unit/__snapshots__/switch.spec.ts.snap | 29 +++++++-------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/test/unit/__snapshots__/switch.spec.ts.snap b/test/unit/__snapshots__/switch.spec.ts.snap index 0fa333d93..615ef7fe1 100644 --- a/test/unit/__snapshots__/switch.spec.ts.snap +++ b/test/unit/__snapshots__/switch.spec.ts.snap @@ -1,15 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`switch empty fallthrough to default (0) 1`] = ` -"require(\\"lualib_bundle\\"); -local ____exports = {} +"local ____exports = {} function ____exports.__main(self) local out = {} repeat local ____switch3 = 0 local ____cond3 = ____switch3 == 1 do - __TS__ArrayPush(out, \\"default\\") + out[#out + 1] = \\"default\\" end until true return out @@ -18,15 +17,14 @@ return ____exports" `; exports[`switch empty fallthrough to default (1) 1`] = ` -"require(\\"lualib_bundle\\"); -local ____exports = {} +"local ____exports = {} function ____exports.__main(self) local out = {} repeat local ____switch3 = 1 local ____cond3 = ____switch3 == 1 do - __TS__ArrayPush(out, \\"default\\") + out[#out + 1] = \\"default\\" end until true return out @@ -35,8 +33,7 @@ return ____exports" `; exports[`switch produces optimal output 1`] = ` -"require(\\"lualib_bundle\\"); -local ____exports = {} +"local ____exports = {} function ____exports.__main(self) local x = 0 local out = {} @@ -44,13 +41,13 @@ function ____exports.__main(self) local ____switch3 = 0 local ____cond3 = ((____switch3 == 0) or (____switch3 == 1)) or (____switch3 == 2) if ____cond3 then - __TS__ArrayPush(out, \\"0,1,2\\") + out[#out + 1] = \\"0,1,2\\" break end ____cond3 = ____cond3 or (____switch3 == 3) if ____cond3 then do - __TS__ArrayPush(out, \\"3\\") + out[#out + 1] = \\"3\\" break end end @@ -60,20 +57,14 @@ function ____exports.__main(self) end do x = x + 1 - __TS__ArrayPush( - out, - \\"default = \\" .. tostring(x) - ) + out[#out + 1] = \\"default = \\" .. tostring(x) do - __TS__ArrayPush(out, \\"3\\") + out[#out + 1] = \\"3\\" break end end until true - __TS__ArrayPush( - out, - tostring(x) - ) + out[#out + 1] = tostring(x) return out end return ____exports" From 8b8d780d13f2ad0286bf0e8950362fb4882d784c Mon Sep 17 00:00:00 2001 From: GlassBricks <24237065+GlassBricks@users.noreply.github.com> Date: Thu, 9 Sep 2021 19:41:41 -0700 Subject: [PATCH 51/51] Add lualib dependency exactness checks --- src/LuaLib.ts | 51 ++++++++++++----------------------- test/transpile/lualib.spec.ts | 51 ++++++++++++++++++++++++++++++++++- tsconfig.json | 2 +- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 00f5786c0..c19cbe87c 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -97,56 +97,39 @@ export enum LuaLibFeature { } /* eslint-disable @typescript-eslint/naming-convention */ -const luaLibDependencies: Partial> = { +export const luaLibDependencies: Readonly>> = { ArrayConcat: [LuaLibFeature.ArrayIsArray], - ArrayFlat: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray], - ArrayFlatMap: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray], - Await: [LuaLibFeature.InstanceOf, LuaLibFeature.New], + ArrayFlat: [LuaLibFeature.ArrayIsArray], + ArrayFlatMap: [LuaLibFeature.ArrayIsArray], + ArrayEntries: [LuaLibFeature.Symbol], + Await: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise], Decorate: [LuaLibFeature.ObjectGetOwnPropertyDescriptor, LuaLibFeature.SetDescriptor, LuaLibFeature.ObjectAssign], - DelegatedYield: [LuaLibFeature.StringAccess], + DelegatedYield: [LuaLibFeature.StringAccess, LuaLibFeature.Symbol], Delete: [LuaLibFeature.ObjectGetOwnPropertyDescriptors], Error: [LuaLibFeature.Class, LuaLibFeature.ClassExtends, LuaLibFeature.New], - FunctionBind: [LuaLibFeature.Unpack], + FunctionBind: [LuaLibFeature.Unpack, LuaLibFeature.ArrayUnshift], Generator: [LuaLibFeature.Symbol], InstanceOf: [LuaLibFeature.Symbol], Iterator: [LuaLibFeature.Symbol], NumberToString: [LuaLibFeature.StringAccess], - ObjectDefineProperty: [LuaLibFeature.CloneDescriptor, LuaLibFeature.SetDescriptor], - ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol], - Promise: [ - LuaLibFeature.ArrayPush, - LuaLibFeature.Class, - LuaLibFeature.FunctionBind, - LuaLibFeature.InstanceOf, - LuaLibFeature.New, - ], + ObjectDefineProperty: [LuaLibFeature.SetDescriptor], + ObjectFromEntries: [LuaLibFeature.Symbol], + Promise: [LuaLibFeature.Class, LuaLibFeature.FunctionBind, LuaLibFeature.InstanceOf, LuaLibFeature.New], PromiseAll: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator], PromiseAllSettled: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator], - PromiseAny: [ - LuaLibFeature.ArrayPush, - LuaLibFeature.InstanceOf, - LuaLibFeature.New, - LuaLibFeature.Promise, - LuaLibFeature.Iterator, - ], - PromiseRace: [ - LuaLibFeature.ArrayPush, - LuaLibFeature.InstanceOf, - LuaLibFeature.New, - LuaLibFeature.Promise, - LuaLibFeature.Iterator, - ], + PromiseAny: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator], + PromiseRace: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator], ParseFloat: [LuaLibFeature.StringAccess], ParseInt: [LuaLibFeature.StringSubstr, LuaLibFeature.StringSubstring], SetDescriptor: [LuaLibFeature.CloneDescriptor], Spread: [LuaLibFeature.Iterator, LuaLibFeature.StringAccess, LuaLibFeature.Unpack], - StringSplit: [LuaLibFeature.StringSubstring, LuaLibFeature.StringAccess], + StringReplaceAll: [LuaLibFeature.StringSplit], SymbolRegistry: [LuaLibFeature.Symbol], - Map: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], - Set: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], - WeakMap: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], - WeakSet: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], + Map: [LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], + Set: [LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], + WeakMap: [LuaLibFeature.Symbol, LuaLibFeature.Class], + WeakSet: [LuaLibFeature.Symbol, LuaLibFeature.Class], }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/test/transpile/lualib.spec.ts b/test/transpile/lualib.spec.ts index 0af391c46..9e357e55a 100644 --- a/test/transpile/lualib.spec.ts +++ b/test/transpile/lualib.spec.ts @@ -1,6 +1,8 @@ import * as ts from "typescript"; import { LuaLibFeature } from "../../src"; -import { loadLuaLibFeatures } from "../../src/LuaLib"; +import { loadLuaLibFeatures, luaLibDependencies } from "../../src/LuaLib"; +import * as path from "path"; +import * as fs from "fs"; test.each(Object.entries(LuaLibFeature))("Lualib feature has correct dependencies (%p)", (_, feature) => { const lualibCode = loadLuaLibFeatures([feature], ts.sys); @@ -24,3 +26,50 @@ test.each(Object.entries(LuaLibFeature))("Lualib feature has correct dependencie expect(missingReferences).toHaveLength(0); }); + +describe("Lualib dependencies match what is needed", () => { + const lualibText = new Map(); + const definedBy = new Map(); + beforeAll(done => { + definedBy.set("Symbol", LuaLibFeature.Symbol); + void Promise.all( + Object.values(LuaLibFeature).map(async feature => { + const featurePath = path.resolve(__dirname, `../../dist/lualib/${feature}.lua`); + const luaLibFeature = await fs.promises.readFile(featurePath, "utf-8"); + lualibText.set(feature, luaLibFeature); + const defines = Array.from( + luaLibFeature.matchAll(/function (__TS__[a-zA-Z_]+)\(|(__TS__[a-zA-Z_]+) =/g) + ).map(match => match[1] ?? match[2]); + for (const define of defines) { + // if duplicate define, will be caught by build-lualib + definedBy.set(define, feature); + } + }) + ).then(() => done()); + }); + + test.each(Object.entries(LuaLibFeature))("%p", (_, feature) => { + const lualibFeature = lualibText.get(feature)!; + + // Find all used lualib references + // __TS__* or Symbol, not surrounded by valid id characters + const luaLibReferences = new Set( + Array.from(lualibFeature.matchAll(/(? match[1] + ) + ); + + const neededDependencies = new Set(); + for (const luaLibReference of luaLibReferences) { + const dependency = definedBy.get(luaLibReference); + if (!dependency) { + throw new Error(`Reference to nonexistent lualib value: ${luaLibReference}`); + } + neededDependencies.add(dependency); + } + neededDependencies.delete(feature); + + const dependencies = new Set(luaLibDependencies[feature]); + expect(dependencies).toEqual(neededDependencies); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index bf121bd20..9e4ebec57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2019", - "lib": ["es2019"], + "lib": ["es2020"], "types": ["node"], "module": "commonjs", "experimentalDecorators": true,