From 4c41fc940466891ec56a844e1c02a2bb47893049 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 29 Aug 2021 15:38:41 -0600 Subject: [PATCH 01/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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 175f85ad4795d5eaec03306c1b3efaebb07367c2 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 10 Sep 2021 14:12:55 -0600 Subject: [PATCH 21/48] additional object destructuring test --- 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 fb27b8b75..81b84fcb4 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -298,6 +298,18 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); + test("object destructuring declaration", () => { + 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; + ({ [getI(i += "D")]: result } = getO(i += "B")); + return [result, i, x]; + `.expectToMatchJsResult(); + }); + test("call expression", () => { util.testFunction` let i = 1; From f16841cc3ee14ee1bcfb8976728d569302de4707 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 15 Sep 2021 07:17:55 -0600 Subject: [PATCH 22/48] fixes for object destructuring --- src/transformation/utils/lua-ast.ts | 1 - .../destructuring-assignments.ts | 82 ++++++++++++++----- .../visitors/variable-declaration.ts | 12 ++- test/unit/precedingStatements.spec.ts | 58 ++++++++++--- 4 files changed, 119 insertions(+), 34 deletions(-) diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index 369ef186f..2561161af 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -187,7 +187,6 @@ export function createLocalOrExportedOrGlobalDeclaration( if (precededDeclaration) { declaration = undefined; } - } else if (rhs) { // global assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); diff --git a/src/transformation/visitors/binary-expression/destructuring-assignments.ts b/src/transformation/visitors/binary-expression/destructuring-assignments.ts index 5dbc4da24..8d9f71ec0 100644 --- a/src/transformation/visitors/binary-expression/destructuring-assignments.ts +++ b/src/transformation/visitors/binary-expression/destructuring-assignments.ts @@ -1,9 +1,11 @@ import * as ts from "typescript"; +import { transformBinaryOperation } from "."; import * as lua from "../../../LuaAST"; -import { assertNever } from "../../../utils"; +import { assertNever, cast } from "../../../utils"; import { TransformationContext } from "../../context"; import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; import { isArrayType, isAssignmentPattern } from "../../utils/typescript"; +import { moveToPrecedingTemp } from "../expression-list"; import { transformPropertyName } from "../literal"; import { transformAssignment, @@ -255,35 +257,73 @@ function transformPropertyAssignment( } } - const leftExpression = ts.isBinaryExpression(node.initializer) ? node.initializer.left : node.initializer; - const variableToExtract = transformPropertyName(context, node.name); + context.pushPrecedingStatements(); + + let variableToExtract = transformPropertyName(context, node.name); + variableToExtract = moveToPrecedingTemp(context, variableToExtract); // Must be evaluated before left's preceding statements const extractingExpression = lua.createTableIndexExpression(root, variableToExtract); - const destructureAssignmentStatements = transformAssignment( - context, - leftExpression, - extractingExpression, - rootHasPrecedingStatements - ); + let destructureAssignmentStatements: lua.Statement[]; + if (ts.isBinaryExpression(node.initializer)) { + if ( + ts.isPropertyAccessExpression(node.initializer.left) || + ts.isElementAccessExpression(node.initializer.left) + ) { + // Access expressions need their table and index expressions cached to preserve execution order + const left = cast(context.transformExpression(node.initializer.left), lua.isTableIndexExpression); - result.push(...destructureAssignmentStatements); + context.pushPrecedingStatements(); + const defaultExpression = context.transformExpression(node.initializer.right); + const defaultPrecedingStatements = context.popPrecedingStatements(); - if (ts.isBinaryExpression(node.initializer)) { - const assignmentLeftHandSide = context.transformExpression(node.initializer.left); + const tableTemp = context.createTempForLuaExpression(left.table); + const indexTemp = context.createTempForLuaExpression(left.index); - const nilCondition = lua.createBinaryExpression( - assignmentLeftHandSide, - lua.createNilLiteral(), - lua.SyntaxKind.EqualityOperator - ); + const tempsDeclaration = lua.createVariableDeclarationStatement( + [tableTemp, indexTemp], + [left.table, left.index] + ); - const ifBlock = lua.createBlock( - transformAssignmentStatement(context, node.initializer as ts.AssignmentExpression) - ); + // obj[index] = extractingExpression ?? defaultExpression + const assignStatement = lua.createAssignmentStatement( + lua.createTableIndexExpression(tableTemp, indexTemp), + transformBinaryOperation( + context, + extractingExpression, + defaultExpression, + ts.SyntaxKind.QuestionQuestionToken, + node.initializer + ) + ); - result.push(lua.createIfStatement(nilCondition, ifBlock, undefined, node)); + destructureAssignmentStatements = [tempsDeclaration, ...defaultPrecedingStatements, assignStatement]; + } else { + const assignmentLeftHandSide = context.transformExpression(node.initializer.left); + + const nilCondition = lua.createBinaryExpression( + assignmentLeftHandSide, + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator + ); + + const ifBlock = lua.createBlock( + transformAssignmentStatement(context, node.initializer as ts.AssignmentExpression) + ); + + destructureAssignmentStatements = [lua.createIfStatement(nilCondition, ifBlock, undefined, node)]; + } + } else { + destructureAssignmentStatements = transformAssignment( + context, + node.initializer, + extractingExpression, + rootHasPrecedingStatements + ); } + result.push(...context.popPrecedingStatements()); + result.push(...destructureAssignmentStatements); + return result; } diff --git a/src/transformation/visitors/variable-declaration.ts b/src/transformation/visitors/variable-declaration.ts index 4782fd157..4802c00d5 100644 --- a/src/transformation/visitors/variable-declaration.ts +++ b/src/transformation/visitors/variable-declaration.ts @@ -63,7 +63,9 @@ export function transformBindingPattern( // The identifier of the new variable const variableName = transformIdentifier(context, element.name); // The field to extract + context.pushPrecedingStatements(); const propertyName = transformPropertyName(context, element.propertyName ?? element.name); + result.push(...context.popPrecedingStatements()); // Keep property's preceding statements in order let expression: lua.Expression; if (element.dotDotDotToken) { @@ -123,11 +125,15 @@ export function transformBindingPattern( result.push(...createLocalOrExportedOrGlobalDeclaration(context, variableName, expression)); if (element.initializer) { const identifier = addExportToIdentifier(context, variableName); + context.pushPrecedingStatements(); + const initializer = context.transformExpression(element.initializer); + const initializerPrecedingStatements = context.popPrecedingStatements(); result.push( lua.createIfStatement( lua.createBinaryExpression(identifier, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator), lua.createBlock([ - lua.createAssignmentStatement(identifier, context.transformExpression(element.initializer)), + ...initializerPrecedingStatements, + lua.createAssignmentStatement(identifier, initializer), ]) ) ); @@ -155,13 +161,15 @@ export function transformBindingVariableDeclaration( table = transformIdentifier(context, initializer); } else { // Contain the expression in a temporary variable - table = lua.createAnonymousIdentifier(); if (initializer) { + table = context.createTempForNode(initializer); let expression = context.transformExpression(initializer); if (isMultiReturnCall(context, initializer)) { expression = wrapInTable(expression); } statements.push(lua.createVariableDeclarationStatement(table, expression)); + } else { + table = lua.createAnonymousIdentifier(); } } statements.push(...transformBindingPattern(context, bindingPattern, table)); diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 81b84fcb4..73783877e 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -278,23 +278,50 @@ describe("execution order", () => { test("object destructuring assignment statement", () => { util.testFunction` let i = "A"; - const o: Record = {ABCDE: "success"}; + const o: Record = {ABCDEFG: "success", result: ""}; function getO(x: string) { i = x + "C"; return o; } + function getO2(x: string) { i = x + "G"; return o; } function getI(x: string) { i = x + "E"; return i; } - const { [getI(i += "D")]: result } = getO(i += "B"); - return [result, i]; + ({ [getI(i += "D")]: getO2(i += "F").result } = getO(i += "B")); + return [i, o]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment statement with default", () => { + util.testFunction` + let i = "A"; + const o: Record = {ABCDEFGHIJ: "success", result: ""}; + function getO(x: string) { i = x + "C"; return o; } + function getO2(x: string) { i = x + "G"; return o; } + function getO3(x: string) { i = x + "I"; return o; } + function getI(x: string): any { i = x + "E"; return undefined; } + ({ [getI(i += "D")]: getO2(i += "F").result = getO3(i += "H")[i += "J"] } = getO(i += "B")); + return [o, i]; `.expectToMatchJsResult(); }); test("object destructuring assignment expression", () => { util.testFunction` let i = "A"; - const o: Record = {ABCDE: "success"}; + const o: Record = {ABCDEFG: "success", result: ""}; function getO(x: string) { i = x + "C"; return o; } + function getO2(x: string) { i = x + "G"; 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, x]; + const x = ({ [getI(i += "D")]: getO2(i += "F").result } = getO(i += "B")); + return [i, o, x]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment expression with default", () => { + util.testFunction` + let i = "A"; + const o: Record = {ABCDEFGHIJ: "success", result: ""}; + function getO(x: string) { i = x + "C"; return o; } + function getO2(x: string) { i = x + "G"; return o; } + function getO3(x: string) { i = x + "I"; return o; } + function getI(x: string): any { i = x + "E"; return undefined; } + const x = ({ [getI(i += "D")]: getO2(i += "F").result = getO3(i += "H")[i += "J"] } = getO(i += "B")); + return [o, i, x]; `.expectToMatchJsResult(); }); @@ -304,9 +331,20 @@ describe("execution order", () => { 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; - ({ [getI(i += "D")]: result } = getO(i += "B")); - return [result, i, x]; + const { [getI(i += "D")]: result } = getO(i += "B"); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("object destructuring declaration with default", () => { + util.testFunction` + let i = "A"; + const o: Record = {ABCDEFGH: "success"}; + function getO(x: string) { i = x + "C"; return o; } + function getO2(x: string) { i = x + "G"; return o; } + function getI(x: string): any { i = x + "E"; return undefined; } + const { [getI(i += "D")]: result = getO2(i += "F")[i += "H"]} = getO(i += "B"); + return [result, i]; `.expectToMatchJsResult(); }); From 6bd5a6f26635ad06eb7287b93609cdd2ed7a1cb0 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 16 Sep 2021 08:55:43 -0600 Subject: [PATCH 23/48] fixed switch statements --- .../visitors/binary-expression/assignments.ts | 9 +- src/transformation/visitors/switch.ts | 104 ++++++++++-------- .../__snapshots__/transformation.spec.ts.snap | 4 +- .../__snapshots__/multi.spec.ts.snap | 2 +- test/unit/precedingStatements.spec.ts | 5 +- 5 files changed, 73 insertions(+), 51 deletions(-) diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index a318c792c..f9dd3c930 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -75,15 +75,16 @@ export function transformAssignment( return [arrayLengthAssignment]; } - const symbol = ts.isShorthandPropertyAssignment(lhs.parent) - ? context.checker.getShorthandAssignmentValueSymbol(lhs.parent) - : context.checker.getSymbolAtLocation(lhs); + const symbol = + lhs.parent && ts.isShorthandPropertyAssignment(lhs.parent) + ? context.checker.getShorthandAssignmentValueSymbol(lhs.parent) + : context.checker.getSymbolAtLocation(lhs); const dependentSymbols = symbol ? getDependenciesOfSymbol(context, symbol) : []; const left = transformAssignmentLeftHandSideExpression(context, lhs, rightHasPrecedingStatements); - const rootAssignment = lua.createAssignmentStatement(left, right, lhs.parent); + const rootAssignment = lua.createAssignmentStatement(left, right, lhs); return [ rootAssignment, diff --git a/src/transformation/visitors/switch.ts b/src/transformation/visitors/switch.ts index ae01ff10d..955a650ac 100644 --- a/src/transformation/visitors/switch.ts +++ b/src/transformation/visitors/switch.ts @@ -18,30 +18,50 @@ const containsBreakOrReturn = (nodes: Iterable): boolean => { }; const coalesceCondition = ( - condition: lua.Expression | undefined, - switchVariable: lua.Identifier, - expression: ts.Expression, - context: TransformationContext -): lua.Expression => { + condition: ts.Expression | undefined, + switchVariable: ts.Identifier, + expression: ts.Expression +): ts.Expression => { + const comparison = ts.factory.createBinaryExpression( + switchVariable, + ts.SyntaxKind.EqualsEqualsEqualsToken, + expression + ); + // Coalesce skipped statements if (condition) { - return lua.createBinaryExpression( - condition, - lua.createBinaryExpression( - switchVariable, - context.transformExpression(expression), - lua.SyntaxKind.EqualityOperator - ), - lua.SyntaxKind.OrOperator - ); + return ts.factory.createBinaryExpression(condition, ts.SyntaxKind.BarBarToken, comparison); } // Next condition - return lua.createBinaryExpression( - switchVariable, - context.transformExpression(expression), - lua.SyntaxKind.EqualityOperator - ); + return comparison; +}; + +const transformCondition = ( + context: TransformationContext, + condition: ts.Expression, + conditionVariable: ts.Identifier, + isInitialCondition: boolean +): lua.Statement[] => { + // Construct TS statements and transform them to ensure preceding statements are handled correctly + if (isInitialCondition) { + // local ____cond = (____switch == x) or (____switch == y) ... + const assignment = ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration(conditionVariable, undefined, undefined, condition)], + ts.NodeFlags.Let + ) + ); + return context.transformStatements(assignment); + } else { + // ____cond = ____cond or (____switch == x) or (____switch == y) ... + const assignment = ts.factory.createAssignment( + conditionVariable, + ts.factory.createBinaryExpression(conditionVariable, ts.SyntaxKind.BarBarToken, condition) + ); + return context.transformStatements(ts.factory.createExpressionStatement(assignment)); + } }; export const transformSwitchStatement: FunctionVisitor = (statement, context) => { @@ -50,8 +70,8 @@ export const transformSwitchStatement: FunctionVisitor = (st // Give the switch and condition accumulator a unique name to prevent nested switches from acting up. const switchName = `____switch${scope.id}`; const conditionName = `____cond${scope.id}`; - const switchVariable = lua.createIdentifier(switchName); - const conditionVariable = lua.createIdentifier(conditionName); + const switchVariable = ts.factory.createIdentifier(switchName); + const conditionVariable = ts.factory.createIdentifier(conditionName); // If the switch only has a default clause, wrap it in a single do. // Otherwise, we need to generate a set of if statements to emulate the switch. @@ -75,7 +95,7 @@ export const transformSwitchStatement: FunctionVisitor = (st // Build up the condition for each if statement let defaultTransformed = false; let isInitialCondition = true; - let condition: lua.Expression | undefined = undefined; + let condition: ts.Expression | undefined = undefined; for (let i = 0; i < clauses.length; i++) { const clause = clauses[i]; const previousClause: ts.CaseOrDefaultClause | undefined = clauses[i - 1]; @@ -88,30 +108,29 @@ export const transformSwitchStatement: FunctionVisitor = (st // Compute the condition for the if statement if (!ts.isDefaultClause(clause)) { - condition = coalesceCondition(condition, switchVariable, clause.expression, context); + condition = coalesceCondition(condition, switchVariable, clause.expression); // Skip empty clauses unless final clause (i.e side-effects) if (i !== clauses.length - 1 && clause.statements.length === 0) continue; // Declare or assign condition variable - statements.push( - isInitialCondition - ? lua.createVariableDeclarationStatement(conditionVariable, condition) - : lua.createAssignmentStatement( - conditionVariable, - lua.createBinaryExpression(conditionVariable, condition, lua.SyntaxKind.OrOperator) - ) - ); + statements.push(...transformCondition(context, condition, conditionVariable, isInitialCondition)); isInitialCondition = false; } else { // If the default is proceeded by empty clauses and will be emitted we may need to initialize the condition if (isInitialCondition) { - statements.push( - lua.createVariableDeclarationStatement( - conditionVariable, - condition ?? lua.createBooleanLiteral(false) - ) - ); + if (condition) { + statements.push( + ...transformCondition(context, condition, conditionVariable, isInitialCondition) + ); + } else { + statements.push( + lua.createVariableDeclarationStatement( + lua.createIdentifier(conditionName), + lua.createBooleanLiteral(false) + ) + ); + } // Clear condition ot ensure it is not evaluated twice condition = undefined; @@ -123,10 +142,7 @@ export const transformSwitchStatement: FunctionVisitor = (st // Evaluate the final condition that we may be skipping if (condition) { statements.push( - lua.createAssignmentStatement( - conditionVariable, - lua.createBinaryExpression(conditionVariable, condition, lua.SyntaxKind.OrOperator) - ) + ...transformCondition(context, condition, conditionVariable, isInitialCondition) ); } continue; @@ -151,7 +167,9 @@ export const transformSwitchStatement: FunctionVisitor = (st } // Push if statement for case - statements.push(lua.createIfStatement(conditionVariable, lua.createBlock(clauseStatements))); + statements.push( + lua.createIfStatement(lua.createIdentifier(conditionName), lua.createBlock(clauseStatements)) + ); // Clear condition for next clause condition = undefined; @@ -204,7 +222,7 @@ export const transformSwitchStatement: FunctionVisitor = (st // Add the switch expression after hoisting const expression = context.transformExpression(statement.expression); - statements.unshift(lua.createVariableDeclarationStatement(switchVariable, expression)); + statements.unshift(lua.createVariableDeclarationStatement(lua.createIdentifier(switchVariable.text), expression)); // Wrap the statements in a repeat until true statement to facilitate dynamic break/returns return lua.createRepeatStatement(lua.createBlock(statements), lua.createBooleanLiteral(true)); diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 7fd968b00..2e0899833 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -4,8 +4,8 @@ exports[`Transformation (blockScopeVariables) 1`] = ` "do local a = 1 local b = 1 - local ____ = {c = 1} - local c = ____.c + local ____temp_0 = {c = 1} + local c = ____temp_0.c end" `; diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index 7d8e10091..908685c6b 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -66,7 +66,7 @@ end" exports[`invalid $multi call (const [a = 0] = $multi()): diagnostics 1`] = `"main.ts(2,25): error TSTL: The $multi function must be called in a return statement."`; exports[`invalid $multi call (const {} = $multi();): code 1`] = ` -"local ____ = { +"local ____temp_0 = { { ____(_G) } diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 73783877e..49d6320f1 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -542,11 +542,14 @@ test("switch scoping", () => { util.testFunction` let i = 0; let x = 0; + let result = ""; switch (x) { case i++: - return i; + result = "test"; + break; case i++: } + return [i, result]; `.expectToMatchJsResult(); }); From 4c154cdf7bd47501d25ca832da471e73d30677c9 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 17 Sep 2021 09:37:42 -0600 Subject: [PATCH 24/48] refactored expression list handling - removed optimization that broke certain situations - optimized out extra temp in common unary operator cases - removed duplicate functionality between transformOrderedExpressions and transformExpressionList --- src/transformation/utils/scope.ts | 1 + .../visitors/expression-list.ts | 194 ++++++++---------- test/unit/__snapshots__/switch.spec.ts.snap | 22 +- .../__snapshots__/deprecated.spec.ts.snap | 3 +- .../__snapshots__/iterable.spec.ts.snap | 6 +- test/unit/precedingStatements.spec.ts | 2 + 6 files changed, 108 insertions(+), 120 deletions(-) diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index bed90da76..d55c55cd2 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -14,6 +14,7 @@ export enum ScopeType { Block = 1 << 5, Try = 1 << 6, Catch = 1 << 7, + ExpressionList = 1 << 8, } interface FunctionDefinitionInfo { diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index d94edbd10..44b8700f3 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -1,19 +1,27 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; -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); +import { popScope, pushScope, Scope, ScopeType } from "../utils/scope"; +import { getSymbolIdOfSymbol } from "../utils/symbols"; + +// Returns true if expressions before this one should be cached to preserve order. +// This is basically a place to check for situations where temps can be optimized out. +function shouldCachePreviousExpressions(context: TransformationContext, expression: ts.Expression, scope: Scope) { + // ++i or i++ where i hasn't been referenced in previous expressions + let symbol: ts.Symbol | undefined; + let symbolId: lua.SymbolId | undefined; + if ( + (ts.isPrefixUnaryExpression(expression) || ts.isPostfixUnaryExpression(expression)) && + ts.isIdentifier(expression.operand) && + (symbol = context.checker.getSymbolAtLocation(expression.operand)) && + (symbolId = getSymbolIdOfSymbol(context, symbol)) && + (!scope.referencedSymbols || !scope.referencedSymbols.has(symbolId)) + ) { + return false; + } + return true; } export function moveToPrecedingTemp(context: TransformationContext, expression: lua.Expression) { @@ -26,62 +34,100 @@ export function moveToPrecedingTemp(context: TransformationContext, expression: return tempClone; } -function processPrecedingStatements( +function transformExpressionsInOrder( context: TransformationContext, - expressionInfo: ExpressionListInfo[], - lastPrecedingStatementsIndex: number -) { - if (lastPrecedingStatementsIndex < 0) { - return; + expressions: readonly ts.Expression[] +): [lua.Expression[], number] { + const scope = pushScope(context, ScopeType.ExpressionList); + + const transformedExpressions: lua.Expression[] = []; + const precedingStatements: lua.Statement[][] = []; + let hasPrecedingStatements = false; + let lastPrecedingStatementsIndex = -1; + for (let i = 0; i < expressions.length; ++i) { + const expression = expressions[i]; + const cachePrevious = shouldCachePreviousExpressions(context, expression, scope); // Must call before transform + + // Transform expression and catch preceding statements + context.pushPrecedingStatements(); + const transformedExpression = context.transformExpression(expression); + transformedExpressions.push(transformedExpression); + const expressionPrecedingStatements = context.popPrecedingStatements(); + precedingStatements.push(expressionPrecedingStatements); + + // Track preceding statements + if (expressionPrecedingStatements.length > 0) { + hasPrecedingStatements = true; + if (cachePrevious) { + lastPrecedingStatementsIndex = i; + } + } + } + + if (!hasPrecedingStatements) { + popScope(context); + return [transformedExpressions, lastPrecedingStatementsIndex]; } - for (let i = 0; i < expressionInfo.length; ++i) { - const info = expressionInfo[i]; + for (let i = 0; i < transformedExpressions.length; ++i) { + let transformedExpression = transformedExpressions[i]; + const expressionPrecedingStatements = precedingStatements[i]; // Bubble up preceding statements - context.addPrecedingStatements(info.precedingStatements); + 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 (includes optimized vararg '...') + // - 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(info.transformedExpression) || - isPrecedingStatementTemp(info) + lua.isLiteral(transformedExpression) || + (expressionPrecedingStatements.length > 0 && lua.isIdentifier(transformedExpression)) ) { continue; } - // Strip 'unpack' from spreads to store in a temp - it will be added back in buildArrayConcatCall, if needed - if (info.isSpread) { - assert(lua.isCallExpression(info.transformedExpression) && info.transformedExpression.params.length === 1); - info.transformedExpression = info.transformedExpression.params[0]; - info.needsUnpack = true; + // Wrap spreads in table to store in a temp + if (ts.isSpreadElement(expressions[i])) { + transformedExpression = wrapInTable(transformedExpression); } - // Inject temp assignment in correct place in preceding statements - info.transformedExpression = moveToPrecedingTemp(context, info.transformedExpression); + transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression); } + + popScope(context); + return [transformedExpressions, lastPrecedingStatementsIndex]; +} + +export function transformOrderedExpressions( + context: TransformationContext, + expressions: readonly ts.Expression[] +): lua.Expression[] { + const [transformedExpressions] = transformExpressionsInOrder(context, expressions); + return transformedExpressions; } -function buildArrayConcatCall(context: TransformationContext, expressionInfo: ExpressionListInfo[]) { +function buildArrayConcatCall( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + lastPrecedingStatementsIndex: number +) { 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); // Optimize '{table.unpack(x)}' to just 'x' - } else { - tbls.push(wrapInTable(...tbl, createUnpackCall(context, info.transformedExpression))); - } + for (let i = 0; i < expressions.length; ++i) { + const transformedExpression = transformedExpressions[i]; + if (ts.isSpreadElement(expressions[i])) { + if (i < lastPrecedingStatementsIndex) { + // Spread statements cached in temps will need to be unpacked + tbls.push(wrapInTable(...tbl, createUnpackCall(context, transformedExpression))); } else { - tbls.push(wrapInTable(...tbl, info.transformedExpression)); + tbls.push(wrapInTable(...tbl, transformedExpression)); } tbl = []; } else { - tbl.push(info.transformedExpression); + tbl.push(transformedExpression); } } if (tbl.length > 0) { @@ -95,70 +141,12 @@ export function transformExpressionList( context: TransformationContext, expressions: readonly ts.Expression[] ): lua.Expression[] { - // Transform expressions and collect info about them - let lastPrecedingStatementsIndex = -1; - const transformListExpression = (expression: ts.Expression, index: number): ExpressionListInfo => { - context.pushPrecedingStatements(); - const transformedExpression = context.transformExpression(expression); - const precedingStatements = context.popPrecedingStatements(); - if (precedingStatements.length > 0) lastPrecedingStatementsIndex = index; - return { transformedExpression, precedingStatements, isSpread: ts.isSpreadElement(expression) }; - }; - const expressionInfo = expressions.map(transformListExpression); - - // Bubble up preceding statements, generating temps when needed to maintain execution order - processPrecedingStatements(context, expressionInfo, lastPrecedingStatementsIndex); + const [transformedExpressions, lastPrecedingStatementsIndex] = transformExpressionsInOrder(context, expressions); // If there are spreads in the middle, use the array concat lib function - const firstSpreadIndex = expressionInfo.findIndex(i => i.isSpread); - if (firstSpreadIndex >= 0 && firstSpreadIndex < expressionInfo.length - 1) { - return buildArrayConcatCall(context, expressionInfo); - } - - return expressionInfo.map(e => e.transformedExpression); -} - -export function transformOrderedExpressions( - context: TransformationContext, - expressions: readonly 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); + const firstSpreadIndex = expressions.findIndex(i => ts.isSpreadElement(i)); + if (firstSpreadIndex >= 0 && firstSpreadIndex < expressions.length - 1) { + return buildArrayConcatCall(context, expressions, transformedExpressions, lastPrecedingStatementsIndex); } return transformedExpressions; diff --git a/test/unit/__snapshots__/switch.spec.ts.snap b/test/unit/__snapshots__/switch.spec.ts.snap index 1349374bb..fbf47c013 100644 --- a/test/unit/__snapshots__/switch.spec.ts.snap +++ b/test/unit/__snapshots__/switch.spec.ts.snap @@ -6,8 +6,8 @@ local ____exports = {} function ____exports.__main(self) local out = {} repeat - local ____switch3 = 0 - local ____cond3 = ____switch3 == 1 + local ____switch4 = 0 + local ____cond4 = ____switch4 == 1 do __TS__ArrayPush(out, \\"default\\") end @@ -23,8 +23,8 @@ local ____exports = {} function ____exports.__main(self) local out = {} repeat - local ____switch3 = 1 - local ____cond3 = ____switch3 == 1 + local ____switch4 = 1 + local ____cond4 = ____switch4 == 1 do __TS__ArrayPush(out, \\"default\\") end @@ -117,21 +117,21 @@ function ____exports.__main(self) local x = 0 local out = {} repeat - local ____switch3 = 0 - local ____cond3 = ((____switch3 == 0) or (____switch3 == 1)) or (____switch3 == 2) - if ____cond3 then + local ____switch4 = 0 + local ____cond4 = ((____switch4 == 0) or (____switch4 == 1)) or (____switch4 == 2) + if ____cond4 then __TS__ArrayPush(out, \\"0,1,2\\") break end - ____cond3 = ____cond3 or (____switch3 == 3) - if ____cond3 then + ____cond4 = ____cond4 or (____switch4 == 3) + if ____cond4 then do __TS__ArrayPush(out, \\"3\\") break end end - ____cond3 = ____cond3 or (____switch3 == 4) - if ____cond3 then + ____cond4 = ____cond4 or (____switch4 == 4) + if ____cond4 then break end do diff --git a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap index 17025dd09..a962a4a30 100644 --- a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap +++ b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap @@ -66,10 +66,9 @@ 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_1[____i_0 + 1] + return arr[____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 75d18e16b..d866c5e31 100644 --- a/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap @@ -7,10 +7,9 @@ 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_1[____i_0 + 1] + local strs = strsArray[____i_0 + 1] if strs then return table.unpack(strs) end @@ -31,10 +30,9 @@ 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_1[____i_0 + 1] + local strs = strsArray[____i_0 + 1] if strs then return table.unpack(strs) end diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 49d6320f1..4c847f28a 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -55,6 +55,8 @@ describe("execution order", () => { "i, ...a, i++, ...[1, i++, 2], i, i++, ...a", "i, inc(), i++", "i, ...[1, i++, inc(), 2], i++", + "[i, ...'foo', i++]", + "[i, ...([1, i++, 2] as any), i++]", ]; test.each(sequenceTests)("array literal ([%p])", sequence => { From 895e58920f7adf5813b9b6e8fddc527593ee93d7 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 18 Sep 2021 07:00:57 -0600 Subject: [PATCH 25/48] optimized out temps in many situations --- src/transformation/utils/scope.ts | 11 ++++ src/transformation/utils/typescript/index.ts | 17 ++++++ .../destructuring-assignments.ts | 3 +- src/transformation/visitors/call.ts | 10 +++- .../visitors/expression-list.ts | 57 ++++++++----------- src/transformation/visitors/literal.ts | 19 +++---- 6 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index d55c55cd2..4211dd5b6 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -68,6 +68,17 @@ export function markSymbolAsReferencedInCurrentScopes( } } +export function getReferenceCountInScope(scope: Scope, symbolId: lua.SymbolId) { + if (!scope.referencedSymbols) { + return 0; + } + const referencedSymbols = scope.referencedSymbols.get(symbolId); + if (!referencedSymbols) { + return 0; + } + return referencedSymbols.length; +} + export function peekScope(context: TransformationContext): Scope { const scopeStack = getScopeStack(context); const scope = scopeStack[scopeStack.length - 1]; diff --git a/src/transformation/utils/typescript/index.ts b/src/transformation/utils/typescript/index.ts index 6d7bfa68c..1e2ffba44 100644 --- a/src/transformation/utils/typescript/index.ts +++ b/src/transformation/utils/typescript/index.ts @@ -92,3 +92,20 @@ export function getFunctionTypeForCall(context: TransformationContext, node: ts. } return context.checker.getTypeFromTypeNode(typeDeclaration.type); } + +export function isConstIdentifier(context: TransformationContext, node: ts.Node) { + let identifier = node; + if (ts.isComputedPropertyName(identifier)) { + identifier = identifier.expression; + } + if (!ts.isIdentifier(identifier)) { + return false; + } + const symbol = context.checker.getSymbolAtLocation(identifier); + if (!symbol || !symbol.declarations) { + return false; + } + return symbol.declarations.some( + d => ts.isVariableDeclarationList(d.parent) && (d.parent.flags & ts.NodeFlags.Const) !== 0 + ); +} diff --git a/src/transformation/visitors/binary-expression/destructuring-assignments.ts b/src/transformation/visitors/binary-expression/destructuring-assignments.ts index 8d9f71ec0..45dd152fe 100644 --- a/src/transformation/visitors/binary-expression/destructuring-assignments.ts +++ b/src/transformation/visitors/binary-expression/destructuring-assignments.ts @@ -260,7 +260,8 @@ function transformPropertyAssignment( context.pushPrecedingStatements(); let variableToExtract = transformPropertyName(context, node.name); - variableToExtract = moveToPrecedingTemp(context, variableToExtract); // Must be evaluated before left's preceding statements + // Must be evaluated before left's preceding statements + variableToExtract = moveToPrecedingTemp(context, variableToExtract, node.name); const extractingExpression = lua.createTableIndexExpression(root, variableToExtract); let destructureAssignmentStatements: lua.Statement[]; diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 1a74ca45d..1eaa52df8 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -80,9 +80,9 @@ function transformCallWithArgPrecedingStatements( // Cache call expression and context arg in temps to preserve execution order if (argPrecedingStatements.length > 0) { - call = moveToPrecedingTemp(context, call); + call = moveToPrecedingTemp(context, call, callExpression); if (callContext) { - args[0] = moveToPrecedingTemp(context, args[0]); + args[0] = moveToPrecedingTemp(context, args[0], callContext); } context.addPrecedingStatements(argPrecedingStatements); } @@ -134,7 +134,11 @@ function transformElementAccessCall( const argPrecedingStatements = context.popPrecedingStatements(); if (argPrecedingStatements.length > 0) { // Cache index in temp if args had preceding statements - index = moveToPrecedingTemp(context, index); + index = moveToPrecedingTemp( + context, + index, + ts.isElementAccessExpression(left) ? left.argumentExpression : left.name + ); context.addPrecedingStatements(argPrecedingStatements); } diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 44b8700f3..c23981976 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -3,28 +3,18 @@ import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; -import { popScope, pushScope, Scope, ScopeType } from "../utils/scope"; -import { getSymbolIdOfSymbol } from "../utils/symbols"; - -// Returns true if expressions before this one should be cached to preserve order. -// This is basically a place to check for situations where temps can be optimized out. -function shouldCachePreviousExpressions(context: TransformationContext, expression: ts.Expression, scope: Scope) { - // ++i or i++ where i hasn't been referenced in previous expressions - let symbol: ts.Symbol | undefined; - let symbolId: lua.SymbolId | undefined; - if ( - (ts.isPrefixUnaryExpression(expression) || ts.isPostfixUnaryExpression(expression)) && - ts.isIdentifier(expression.operand) && - (symbol = context.checker.getSymbolAtLocation(expression.operand)) && - (symbolId = getSymbolIdOfSymbol(context, symbol)) && - (!scope.referencedSymbols || !scope.referencedSymbols.has(symbolId)) - ) { - return false; - } - return true; -} +import { getReferenceCountInScope, popScope, pushScope, ScopeType } from "../utils/scope"; +import { isConstIdentifier } from "../utils/typescript"; -export function moveToPrecedingTemp(context: TransformationContext, expression: lua.Expression) { +// Cache an expression in a preceding statement and return the temp identifier +export function moveToPrecedingTemp( + context: TransformationContext, + expression: lua.Expression, + tsOriginal?: ts.Node +): lua.Expression { + if (lua.isLiteral(expression) || (tsOriginal && isConstIdentifier(context, tsOriginal))) { + return expression; + } const tempIdentifier = context.createTempForLuaExpression(expression); const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression); lua.setNodePosition(tempDeclaration, lua.getOriginalPos(expression)); @@ -38,15 +28,14 @@ function transformExpressionsInOrder( context: TransformationContext, expressions: readonly ts.Expression[] ): [lua.Expression[], number] { + // Use a custom scope to track variable references const scope = pushScope(context, ScopeType.ExpressionList); const transformedExpressions: lua.Expression[] = []; const precedingStatements: lua.Statement[][] = []; - let hasPrecedingStatements = false; let lastPrecedingStatementsIndex = -1; for (let i = 0; i < expressions.length; ++i) { const expression = expressions[i]; - const cachePrevious = shouldCachePreviousExpressions(context, expression, scope); // Must call before transform // Transform expression and catch preceding statements context.pushPrecedingStatements(); @@ -57,20 +46,19 @@ function transformExpressionsInOrder( // Track preceding statements if (expressionPrecedingStatements.length > 0) { - hasPrecedingStatements = true; - if (cachePrevious) { - lastPrecedingStatementsIndex = i; - } + lastPrecedingStatementsIndex = i; } } - if (!hasPrecedingStatements) { + // No need for extra processing if there were no preceding statements generated + if (lastPrecedingStatementsIndex === -1) { popScope(context); return [transformedExpressions, lastPrecedingStatementsIndex]; } for (let i = 0; i < transformedExpressions.length; ++i) { let transformedExpression = transformedExpressions[i]; + const expression = expressions[i]; const expressionPrecedingStatements = precedingStatements[i]; // Bubble up preceding statements @@ -78,28 +66,29 @@ function transformExpressionsInOrder( // 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 + // - Expression is an identifier that hasn't been referenced more than once if ( i >= lastPrecedingStatementsIndex || - lua.isLiteral(transformedExpression) || - (expressionPrecedingStatements.length > 0 && lua.isIdentifier(transformedExpression)) + (lua.isIdentifier(transformedExpression) && + transformedExpression.symbolId && + getReferenceCountInScope(scope, transformedExpression.symbolId) <= 1) ) { continue; } // Wrap spreads in table to store in a temp - if (ts.isSpreadElement(expressions[i])) { + if (ts.isSpreadElement(expression)) { transformedExpression = wrapInTable(transformedExpression); } - transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression); + transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression, expression); } popScope(context); return [transformedExpressions, lastPrecedingStatementsIndex]; } +// Transforms a series of expressions while maintaining execution order export function transformOrderedExpressions( context: TransformationContext, expressions: readonly ts.Expression[] diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 828838262..15ab60434 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -70,6 +70,7 @@ const transformObjectLiteralExpression: 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)) - ) { - property.value = moveToPrecedingTemp(context, property.value); - } + property.value = moveToPrecedingTemp(context, property.value, initializers[i]); } else { - properties[i] = moveToPrecedingTemp(context, property); + properties[i] = moveToPrecedingTemp(context, property, initializers[i]); } } } From 13c6d1af91e543c38f50c1216e475c39b90965b4 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 18 Sep 2021 07:51:15 -0600 Subject: [PATCH 26/48] fixed execution order of computed property names in object literals --- src/transformation/visitors/literal.ts | 45 ++++++++++++++++++-------- test/unit/precedingStatements.spec.ts | 8 +++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 15ab60434..65aeac119 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -71,13 +71,25 @@ const transformObjectLiteralExpression: FunctionVisitor 0) { + lastPrecedingStatementsIndex = i; + } + + // Transform value and cache preceding statements context.pushPrecedingStatements(); if (ts.isPropertyAssignment(element)) { @@ -119,9 +131,9 @@ const transformObjectLiteralExpression: FunctionVisitor 0) { + precedingStatements = context.popPrecedingStatements(); + valuePrecedingStatements.push(precedingStatements); + if (precedingStatements.length > 0) { lastPrecedingStatementsIndex = i; } } @@ -131,17 +143,24 @@ const transformObjectLiteralExpression: FunctionVisitor= lastPrecedingStatementsIndex) continue; + // Bubble up value's preceding statements + context.addPrecedingStatements(valuePrecedingStatements[i]); - if (lua.isTableFieldExpression(property)) { - property.value = moveToPrecedingTemp(context, property.value, initializers[i]); - } else { - properties[i] = moveToPrecedingTemp(context, property, initializers[i]); + // Cache property value in temp if before the last expression that generated preceding statements + if (i < lastPrecedingStatementsIndex) { + if (lua.isTableFieldExpression(property)) { + property.value = moveToPrecedingTemp(context, property.value, initializers[i]); + } else { + properties[i] = moveToPrecedingTemp(context, property, initializers[i]); + } } } } diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 4c847f28a..d259d5629 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -99,6 +99,14 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); + test("object literal with computed property names", () => { + util.testFunction` + let i = "A"; + const o = {[i += "B"]: i += "C", [i += "D"]: i += "E"}; + return [i, o]; + `.expectToMatchJsResult(); + }); + test("comma operator", () => { util.testFunction` let a = 0, b = 0, c = 0; From 65c6131b7a593019ea78bda3e095e3387e3f7fdf Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 19 Sep 2021 07:53:09 -0600 Subject: [PATCH 27/48] a little refactoring --- src/transformation/utils/lua-ast.ts | 37 ++++++++++---------------- src/transformation/utils/scope.ts | 7 +++++ src/transformation/visitors/literal.ts | 2 +- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index 2561161af..f5f6f1bf7 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -4,7 +4,7 @@ import * as lua from "../../LuaAST"; import { assert, castArray } from "../../utils"; import { TransformationContext } from "../context"; import { createExportedIdentifier, getIdentifierExportScope } from "./export"; -import { peekScope, ScopeType, Scope } from "./scope"; +import { peekScope, ScopeType, Scope, addScopeVariableDeclaration } from "./scope"; import { transformLuaLibFunction } from "./lualib"; import { LuaLibFeature } from "../../LuaLib"; @@ -106,12 +106,7 @@ export function createHoistableVariableDeclarationStatement( if (identifier.symbolId !== undefined) { const scope = peekScope(context); assert(scope.type !== ScopeType.Switch); - - if (!scope.variableDeclarations) { - scope.variableDeclarations = []; - } - - scope.variableDeclarations.push(declaration); + addScopeVariableDeclaration(scope, declaration); } return declaration; @@ -162,30 +157,26 @@ export function createLocalOrExportedOrGlobalDeclaration( const isTopLevelVariable = scope.type === ScopeType.File; if (context.isModule || !isTopLevelVariable) { - let precededDeclaration = false; if (!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; + // Split declaration and assignment of identifiers that reference themselves in their declaration. + // Put declaration above preceding statements in case the identifier is referenced in those. + const precedingDeclaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal); + context.addPrecedingStatements([precedingDeclaration], true); if (rhs) { assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); } + + if (!isFunctionDeclaration) { + // Remember local variable declarations for hoisting later + addScopeVariableDeclaration(scope, precedingDeclaration); + } } else { declaration = lua.createVariableDeclarationStatement(lhs, rhs, tsOriginal); - } - if (!isFunctionDeclaration) { - // Remember local variable declarations for hoisting later - if (!scope.variableDeclarations) { - scope.variableDeclarations = []; + if (!isFunctionDeclaration) { + // Remember local variable declarations for hoisting later + addScopeVariableDeclaration(scope, declaration); } - - scope.variableDeclarations.push(declaration); - } - - if (precededDeclaration) { - declaration = undefined; } } else if (rhs) { // global diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index 4211dd5b6..a71ad075b 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -110,6 +110,13 @@ export function popScope(context: TransformationContext): Scope { return scope; } +export function addScopeVariableDeclaration(scope: Scope, declaration: lua.VariableDeclarationStatement) { + if (!scope.variableDeclarations) { + scope.variableDeclarations = []; + } + scope.variableDeclarations.push(declaration); +} + function isHoistableFunctionDeclaredInScope(symbol: ts.Symbol, scopeNode: ts.Node) { return symbol?.declarations?.some( d => ts.isFunctionDeclaration(d) && findFirstNodeAbove(d, (n): n is ts.Node => n === scopeNode) diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index 65aeac119..32576a815 100644 --- a/src/transformation/visitors/literal.ts +++ b/src/transformation/visitors/literal.ts @@ -109,7 +109,7 @@ const transformObjectLiteralExpression: FunctionVisitor Date: Fri, 24 Sep 2021 07:04:23 -0600 Subject: [PATCH 28/48] removed bad optimization and refactored call stuff a bit --- src/transformation/utils/scope.ts | 1 - .../visitors/binary-expression/assignments.ts | 2 + src/transformation/visitors/call.ts | 119 ++++++------------ .../visitors/expression-list.ts | 17 +-- test/unit/__snapshots__/switch.spec.ts.snap | 22 ++-- .../__snapshots__/table.spec.ts.snap | 10 +- test/unit/precedingStatements.spec.ts | 15 ++- 7 files changed, 71 insertions(+), 115 deletions(-) diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index a71ad075b..02a8e31b2 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -14,7 +14,6 @@ export enum ScopeType { Block = 1 << 5, Try = 1 << 6, Catch = 1 << 7, - ExpressionList = 1 << 8, } interface FunctionDefinitionInfo { diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index f9dd3c930..adc09378f 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -196,6 +196,8 @@ const canBeTransformedToLuaAssignmentStatement = ( } if (ts.isPropertyAccessExpression(element) || ts.isElementAccessExpression(element)) { + // Lua's execution order for multi-assignments is not the same as JS's, so we should always + // break these down when the left side may have side effects. return false; } diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 1eaa52df8..49ee05505 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -27,67 +27,33 @@ import { moveToPrecedingTemp, transformExpressionList } from "./expression-list" export type PropertyCallExpression = ts.CallExpression & { expression: ts.PropertyAccessExpression }; -export function transformArguments( +export function validateArguments( context: TransformationContext, params: readonly ts.Expression[], - 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)); + signature?: ts.Signature +) { + if (!signature || signature.parameters.length < params.length) { + return; } - - // 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]; - const paramType = context.checker.getTypeAtLocation(param); - if (signatureParameter.valueDeclaration !== undefined) { - const signatureType = context.checker.getTypeAtLocation(signatureParameter.valueDeclaration); - validateAssignment(context, param, paramType, signatureType, signatureParameter.name); - } + for (const [index, param] of params.entries()) { + const signatureParameter = signature.parameters[index]; + const paramType = context.checker.getTypeAtLocation(param); + if (signatureParameter.valueDeclaration !== undefined) { + const signatureType = context.checker.getTypeAtLocation(signatureParameter.valueDeclaration); + validateAssignment(context, param, paramType, signatureType, signatureParameter.name); } } - - return parameters; } -function transformCallWithArgPrecedingStatements( +export function transformArguments( context: TransformationContext, - callExpression: ts.Expression, - args: lua.Expression[], - argPrecedingStatements: lua.Statement[], + params: readonly ts.Expression[], + signature?: ts.Signature, 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, callExpression); - if (callContext) { - args[0] = moveToPrecedingTemp(context, args[0], callContext); - } - context.addPrecedingStatements(argPrecedingStatements); - } - - return [call, args]; +): lua.Expression[] { + validateArguments(context, params, signature); + const parameters = transformExpressionList(context, callContext ? [callContext, ...params] : params); + return parameters; } export function transformCallAndArguments( @@ -97,10 +63,15 @@ export function transformCallAndArguments( signature?: ts.Signature, callContext?: ts.Expression ): [lua.Expression, lua.Expression[]] { + let call = context.transformExpression(callExpression); context.pushPrecedingStatements(); - const args = transformArguments(context, params, signature, callContext); - const precedingStatements = context.popPrecedingStatements(); - return transformCallWithArgPrecedingStatements(context, callExpression, args, precedingStatements); + const parameters = transformArguments(context, params, signature, callContext); + const paramPrecedingStatements = context.popPrecedingStatements(); + if (paramPrecedingStatements.length > 0) { + context.addPrecedingStatements(paramPrecedingStatements); + call = moveToPrecedingTemp(context, call, callExpression); + } + return [call, parameters]; } function transformElementAccessCall( @@ -134,11 +105,7 @@ function transformElementAccessCall( const argPrecedingStatements = context.popPrecedingStatements(); if (argPrecedingStatements.length > 0) { // Cache index in temp if args had preceding statements - index = moveToPrecedingTemp( - context, - index, - ts.isElementAccessExpression(left) ? left.argumentExpression : left.name - ); + index = moveToPrecedingTemp(context, index); context.addPrecedingStatements(argPrecedingStatements); } @@ -153,18 +120,9 @@ export function transformContextualCallExpression( ): lua.CallExpression | lua.MethodCallExpression { const left = ts.isCallExpression(node) ? node.expression : node.tag; - 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 - ) { + if (ts.isPropertyAccessExpression(left) && ts.isIdentifier(left.name) && isValidLuaIdentifier(left.name.text)) { // table:name() - const table = context.transformExpression(left.expression); + const [table, ...transformedArguments] = transformExpressionList(context, [left.expression, ...args]); if (ts.isOptionalChain(node)) { return transformLuaLibFunction( @@ -188,24 +146,25 @@ export function transformContextualCallExpression( if (isExpressionWithEvaluationEffect(left.expression)) { return transformElementAccessCall(context, left, args, signature); } else { - const [expression, updatedArgs] = transformCallWithArgPrecedingStatements( + const [expression, transformedArguments] = transformCallAndArguments( context, left, - transformedArguments, - argPrecedingStatements, + args, + signature, left.expression ); - return lua.createCallExpression(expression, updatedArgs, node); + return lua.createCallExpression(expression, transformedArguments, node); } } else if (ts.isIdentifier(left)) { - const callContext = context.isStrict ? lua.createNilLiteral() : lua.createIdentifier("_G"); - const [expression, updatedArgs] = transformCallWithArgPrecedingStatements( + const callContext = context.isStrict ? ts.factory.createNull() : ts.factory.createIdentifier("_G"); + const [expression, transformedArguments] = transformCallAndArguments( context, left, - transformedArguments, - argPrecedingStatements + args, + signature, + callContext ); - return lua.createCallExpression(expression, [callContext, ...updatedArgs], node); + return lua.createCallExpression(expression, transformedArguments, node); } else { throw new Error(`Unsupported LeftHandSideExpression kind: ${ts.SyntaxKind[left.kind]}`); } diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index c23981976..a64678986 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -3,7 +3,6 @@ import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; -import { getReferenceCountInScope, popScope, pushScope, ScopeType } from "../utils/scope"; import { isConstIdentifier } from "../utils/typescript"; // Cache an expression in a preceding statement and return the temp identifier @@ -28,9 +27,6 @@ function transformExpressionsInOrder( context: TransformationContext, expressions: readonly ts.Expression[] ): [lua.Expression[], number] { - // Use a custom scope to track variable references - const scope = pushScope(context, ScopeType.ExpressionList); - const transformedExpressions: lua.Expression[] = []; const precedingStatements: lua.Statement[][] = []; let lastPrecedingStatementsIndex = -1; @@ -52,7 +48,6 @@ function transformExpressionsInOrder( // No need for extra processing if there were no preceding statements generated if (lastPrecedingStatementsIndex === -1) { - popScope(context); return [transformedExpressions, lastPrecedingStatementsIndex]; } @@ -64,15 +59,8 @@ function transformExpressionsInOrder( // 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 an identifier that hasn't been referenced more than once - if ( - i >= lastPrecedingStatementsIndex || - (lua.isIdentifier(transformedExpression) && - transformedExpression.symbolId && - getReferenceCountInScope(scope, transformedExpression.symbolId) <= 1) - ) { + // No need to cache at or after last expression which generated preceding statements + if (i >= lastPrecedingStatementsIndex) { continue; } @@ -84,7 +72,6 @@ function transformExpressionsInOrder( transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression, expression); } - popScope(context); return [transformedExpressions, lastPrecedingStatementsIndex]; } diff --git a/test/unit/__snapshots__/switch.spec.ts.snap b/test/unit/__snapshots__/switch.spec.ts.snap index fbf47c013..1349374bb 100644 --- a/test/unit/__snapshots__/switch.spec.ts.snap +++ b/test/unit/__snapshots__/switch.spec.ts.snap @@ -6,8 +6,8 @@ local ____exports = {} function ____exports.__main(self) local out = {} repeat - local ____switch4 = 0 - local ____cond4 = ____switch4 == 1 + local ____switch3 = 0 + local ____cond3 = ____switch3 == 1 do __TS__ArrayPush(out, \\"default\\") end @@ -23,8 +23,8 @@ local ____exports = {} function ____exports.__main(self) local out = {} repeat - local ____switch4 = 1 - local ____cond4 = ____switch4 == 1 + local ____switch3 = 1 + local ____cond3 = ____switch3 == 1 do __TS__ArrayPush(out, \\"default\\") end @@ -117,21 +117,21 @@ function ____exports.__main(self) local x = 0 local out = {} repeat - local ____switch4 = 0 - local ____cond4 = ((____switch4 == 0) or (____switch4 == 1)) or (____switch4 == 2) - if ____cond4 then + local ____switch3 = 0 + local ____cond3 = ((____switch3 == 0) or (____switch3 == 1)) or (____switch3 == 2) + if ____cond3 then __TS__ArrayPush(out, \\"0,1,2\\") break end - ____cond4 = ____cond4 or (____switch4 == 3) - if ____cond4 then + ____cond3 = ____cond3 or (____switch3 == 3) + if ____cond3 then do __TS__ArrayPush(out, \\"3\\") break end end - ____cond4 = ____cond4 or (____switch4 == 4) - if ____cond4 then + ____cond3 = ____cond3 or (____switch3 == 4) + if ____cond3 then break end do diff --git a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap index 46d14f22b..31daea00d 100644 --- a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap @@ -68,9 +68,10 @@ 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`] = ` -"local ____foo_0 = foo; +"local ____foo_1 = foo +local _____G_0 = _G; ({}).foo = nil -____foo_0(_G, nil)" +____foo_1(_____G_0, 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."`; @@ -97,9 +98,10 @@ 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`] = ` -"local ____foo_0 = foo; +"local ____foo_1 = foo +local _____G_0 = _G; ({}).foo = 3 -____foo_0(_G, nil)" +____foo_1(_____G_0, 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 d259d5629..c18e08623 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -75,7 +75,9 @@ describe("execution order", () => { function inc() { ++i; return i; } function foo(...args: unknown[]) { return args; } return foo(${sequence}); - `.expectToMatchJsResult(); + ` + .debug() + .expectToMatchJsResult(); }); test.each([ @@ -516,8 +518,13 @@ describe("loop expressions", () => { test("for loop", () => { util.testFunction` - let i: number, j: number; - for (i = 0, j = 0; i++ < 5 && j < 10; ++j) {} + let j = 0; + for (let i = 0; i++ < 5;) { + ++j; + if (j >= 10) { + break; + } + } return i; `.expectToMatchJsResult(); }); @@ -548,7 +555,7 @@ describe("loop expressions", () => { }); }); -test("switch scoping", () => { +test("switch", () => { util.testFunction` let i = 0; let x = 0; From 0896dd568be203fc0989321e594b0c0c49cfc032 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 28 Sep 2021 12:11:34 -0600 Subject: [PATCH 29/48] added SparseArray lib functions to handle complex expression lists and fixed/refactored a few more things --- src/LuaLib.ts | 3 + src/lualib/SparseArrayNew.ts | 6 + src/lualib/SparseArrayPush.ts | 8 + src/lualib/SparseArraySpread.ts | 4 + src/transformation/context/context.ts | 1 + src/transformation/visitors/call.ts | 89 ++++++--- .../visitors/expression-list.ts | 189 ++++++++++++------ src/transformation/visitors/sourceFile.ts | 5 +- test/unit/precedingStatements.spec.ts | 10 +- 9 files changed, 211 insertions(+), 104 deletions(-) create mode 100644 src/lualib/SparseArrayNew.ts create mode 100644 src/lualib/SparseArrayPush.ts create mode 100644 src/lualib/SparseArraySpread.ts diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 03483d2cc..a03f2b447 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -70,6 +70,9 @@ export enum LuaLibFeature { PromiseRace = "PromiseRace", Set = "Set", SetDescriptor = "SetDescriptor", + SparseArrayNew = "SparseArrayNew", + SparseArrayPush = "SparseArrayPush", + SparseArraySpread = "SparseArraySpread", WeakMap = "WeakMap", WeakSet = "WeakSet", SourceMapTraceBack = "SourceMapTraceBack", diff --git a/src/lualib/SparseArrayNew.ts b/src/lualib/SparseArrayNew.ts new file mode 100644 index 000000000..d7f8d9c8d --- /dev/null +++ b/src/lualib/SparseArrayNew.ts @@ -0,0 +1,6 @@ +type __TS__SparseArray = unknown[] & { "#": number }; + +function __TS__SparseArrayNew(this: void, ...args: unknown[]) { + (args as __TS__SparseArray)["#"] = select("#", ...args); + return args; +} diff --git a/src/lualib/SparseArrayPush.ts b/src/lualib/SparseArrayPush.ts new file mode 100644 index 000000000..e077432c6 --- /dev/null +++ b/src/lualib/SparseArrayPush.ts @@ -0,0 +1,8 @@ +function __TS__SparseArrayPush(this: void, list: unknown[], ...args: unknown[]) { + const argsLen = select("#", ...args); + const listLen = (list as __TS__SparseArray)["#"]; + for (const i of $range(1, argsLen)) { + list[listLen + i - 1] = args[i - 1]; + } + (list as __TS__SparseArray)["#"] = listLen + argsLen; +} diff --git a/src/lualib/SparseArraySpread.ts b/src/lualib/SparseArraySpread.ts new file mode 100644 index 000000000..08a23b7d3 --- /dev/null +++ b/src/lualib/SparseArraySpread.ts @@ -0,0 +1,4 @@ +function __TS__SparseArraySpread(this: void, list: unknown[]) { + const _unpack = unpack ?? table.unpack; + return _unpack(list, 1, (list as __TS__SparseArray)["#"]); +} diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index d7a6fa8ae..aa2059a2a 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -136,6 +136,7 @@ export class TransformationContext { public addPrecedingStatements(statements: lua.Statement[], prepend = false) { const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; + assert(precedingStatements); if (prepend) { precedingStatements.unshift(...statements); } else { diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 49ee05505..1abe130db 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -52,8 +52,36 @@ export function transformArguments( callContext?: ts.Expression ): lua.Expression[] { validateArguments(context, params, signature); - const parameters = transformExpressionList(context, callContext ? [callContext, ...params] : params); - return parameters; + return transformExpressionList(context, callContext ? [callContext, ...params] : params); +} + +function transformCallWithArguments( + context: TransformationContext, + callExpression: ts.Expression, + transformedArguments: lua.Expression[], + argPrecedingStatements: lua.Statement[], + callContext?: ts.Expression +): [lua.Expression, lua.Expression[]] { + let call = context.transformExpression(callExpression); + + let transformedContext: lua.Expression | undefined; + if (callContext) { + transformedContext = context.transformExpression(callContext); + } + + if (argPrecedingStatements.length > 0) { + if (transformedContext) { + transformedContext = moveToPrecedingTemp(context, transformedContext, callContext); + } + call = moveToPrecedingTemp(context, call, callExpression); + context.addPrecedingStatements(argPrecedingStatements); + } + + if (transformedContext) { + transformedArguments.unshift(transformedContext); + } + + return [call, transformedArguments]; } export function transformCallAndArguments( @@ -63,22 +91,17 @@ export function transformCallAndArguments( signature?: ts.Signature, callContext?: ts.Expression ): [lua.Expression, lua.Expression[]] { - let call = context.transformExpression(callExpression); context.pushPrecedingStatements(); - const parameters = transformArguments(context, params, signature, callContext); - const paramPrecedingStatements = context.popPrecedingStatements(); - if (paramPrecedingStatements.length > 0) { - context.addPrecedingStatements(paramPrecedingStatements); - call = moveToPrecedingTemp(context, call, callExpression); - } - return [call, parameters]; + const transformedArguments = transformArguments(context, params, signature, callContext); + const argPrecedingStatements = context.popPrecedingStatements(); + return transformCallWithArguments(context, callExpression, transformedArguments, argPrecedingStatements); } function transformElementAccessCall( context: TransformationContext, left: ts.PropertyAccessExpression | ts.ElementAccessExpression, - args: ts.Expression[] | ts.NodeArray, - signature?: ts.Signature + transformedArguments: lua.Expression[], + argPrecedingStatements: lua.Statement[] ) { // Cache left-side if it has effects // local ____self = context; return ____self[argument](parameters); @@ -93,23 +116,13 @@ function transformElementAccessCall( let index: lua.Expression = lua.createTableIndexExpression(selfIdentifier, argument); - context.pushPrecedingStatements(); - - const transformedArguments = transformArguments( - context, - args, - signature, - ts.factory.createIdentifier(selfIdentifier.text) - ); - - 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); + return lua.createCallExpression(index, [selfIdentifier, ...transformedArguments]); } export function transformContextualCallExpression( @@ -120,10 +133,18 @@ export function transformContextualCallExpression( ): 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)) { - // table:name() - const [table, ...transformedArguments] = transformExpressionList(context, [left.expression, ...args]); + context.pushPrecedingStatements(); + let 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); if (ts.isOptionalChain(node)) { return transformLuaLibFunction( context, @@ -144,24 +165,26 @@ export function transformContextualCallExpression( } } else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) { if (isExpressionWithEvaluationEffect(left.expression)) { - return transformElementAccessCall(context, left, args, signature); + return transformElementAccessCall(context, left, transformedArguments, argPrecedingStatements); } else { - const [expression, transformedArguments] = transformCallAndArguments( + let expression: lua.Expression; + [expression, transformedArguments] = transformCallWithArguments( context, left, - args, - signature, + transformedArguments, + argPrecedingStatements, left.expression ); return lua.createCallExpression(expression, transformedArguments, node); } } else if (ts.isIdentifier(left)) { const callContext = context.isStrict ? ts.factory.createNull() : ts.factory.createIdentifier("_G"); - const [expression, transformedArguments] = transformCallAndArguments( + let expression: lua.Expression; + [expression, transformedArguments] = transformCallWithArguments( context, left, - args, - signature, + transformedArguments, + argPrecedingStatements, callContext ); return lua.createCallExpression(expression, transformedArguments, node); diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index a64678986..21b6710db 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -1,7 +1,7 @@ +import assert = require("assert"); import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; -import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { isConstIdentifier } from "../utils/typescript"; @@ -23,93 +23,120 @@ export function moveToPrecedingTemp( return tempClone; } -function transformExpressionsInOrder( +function shouldMoveToTemp( + context: TransformationContext, + expression: ts.Expression, + transformedExpression: lua.Expression +) { + return !lua.isLiteral(transformedExpression) && !isConstIdentifier(context, expression); +} + +function transformExpressions( context: TransformationContext, expressions: readonly ts.Expression[] -): [lua.Expression[], number] { - const transformedExpressions: lua.Expression[] = []; +): [lua.Expression[], lua.Statement[][], number] { const precedingStatements: lua.Statement[][] = []; + const transformExpressions: lua.Expression[] = []; let lastPrecedingStatementsIndex = -1; for (let i = 0; i < expressions.length; ++i) { - const expression = expressions[i]; - - // Transform expression and catch preceding statements context.pushPrecedingStatements(); - const transformedExpression = context.transformExpression(expression); - transformedExpressions.push(transformedExpression); + transformExpressions.push(context.transformExpression(expressions[i])); const expressionPrecedingStatements = context.popPrecedingStatements(); - precedingStatements.push(expressionPrecedingStatements); - - // Track preceding statements if (expressionPrecedingStatements.length > 0) { lastPrecedingStatementsIndex = i; } + precedingStatements.push(expressionPrecedingStatements); } + return [transformExpressions, precedingStatements, lastPrecedingStatementsIndex]; +} - // No need for extra processing if there were no preceding statements generated - if (lastPrecedingStatementsIndex === -1) { - return [transformedExpressions, lastPrecedingStatementsIndex]; - } - +function transformExpressionsUsingTemps( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + precedingStatements: lua.Statement[][], + lastPrecedingStatementsIndex: number +) { for (let i = 0; i < transformedExpressions.length; ++i) { - let transformedExpression = transformedExpressions[i]; - const expression = expressions[i]; - const expressionPrecedingStatements = precedingStatements[i]; - - // Bubble up preceding statements - context.addPrecedingStatements(expressionPrecedingStatements); - - // No need to cache at or after last expression which generated preceding statements - if (i >= lastPrecedingStatementsIndex) { - continue; - } + context.addPrecedingStatements(precedingStatements[i]); - // Wrap spreads in table to store in a temp - if (ts.isSpreadElement(expression)) { - transformedExpression = wrapInTable(transformedExpression); + const transformedExpression = transformedExpressions[i]; + const expression = expressions[i]; + if (i < lastPrecedingStatementsIndex && shouldMoveToTemp(context, expression, transformedExpression)) { + transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression, expression); } - - transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression, expression); } - return [transformedExpressions, lastPrecedingStatementsIndex]; + return transformedExpressions; } -// Transforms a series of expressions while maintaining execution order -export function transformOrderedExpressions( +function pushToList( context: TransformationContext, - expressions: readonly ts.Expression[] -): lua.Expression[] { - const [transformedExpressions] = transformExpressionsInOrder(context, expressions); - return transformedExpressions; + listIdentifier: lua.Identifier | undefined, + expressions: lua.Expression[] +) { + if (!listIdentifier) { + listIdentifier = lua.createIdentifier(context.createTempName("list")); + const libCall = transformLuaLibFunction(context, LuaLibFeature.SparseArrayNew, undefined, ...expressions); + const declaration = lua.createVariableDeclarationStatement(listIdentifier, libCall); + context.addPrecedingStatements([declaration]); + } else { + const libCall = transformLuaLibFunction( + context, + LuaLibFeature.SparseArrayPush, + undefined, + listIdentifier, + ...expressions + ); + context.addPrecedingStatements([lua.createExpressionStatement(libCall)]); + } + return listIdentifier; } -function buildArrayConcatCall( +function transformExpressionsUsingList( context: TransformationContext, expressions: readonly ts.Expression[], transformedExpressions: lua.Expression[], - lastPrecedingStatementsIndex: number + precedingStatements: lua.Statement[][] ) { - const tbls: lua.Expression[] = []; - let tbl: lua.Expression[] = []; + let listIdentifier: lua.Identifier | undefined; + + let expressionSet: lua.Expression[] = []; for (let i = 0; i < expressions.length; ++i) { - const transformedExpression = transformedExpressions[i]; + if (precedingStatements[i].length > 0 && expressionSet.length > 0) { + listIdentifier = pushToList(context, listIdentifier, expressionSet); + expressionSet = []; + } + + context.addPrecedingStatements(precedingStatements[i]); + expressionSet.push(transformedExpressions[i]); + if (ts.isSpreadElement(expressions[i])) { - if (i < lastPrecedingStatementsIndex) { - // Spread statements cached in temps will need to be unpacked - tbls.push(wrapInTable(...tbl, createUnpackCall(context, transformedExpression))); - } else { - tbls.push(wrapInTable(...tbl, transformedExpression)); - } - tbl = []; - } else { - tbl.push(transformedExpression); + listIdentifier = pushToList(context, listIdentifier, expressionSet); + expressionSet = []; } } - if (tbl.length > 0) { - tbls.push(wrapInTable(...tbl)); + + if (expressionSet.length > 0) { + listIdentifier = pushToList(context, listIdentifier, expressionSet); + } + + assert(listIdentifier); + return [transformLuaLibFunction(context, LuaLibFeature.SparseArraySpread, undefined, listIdentifier)]; +} + +function countTempCandidates( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + lastPrecedingStatementsIndex: number +) { + if (lastPrecedingStatementsIndex < 0) { + return 0; } - return [createUnpackCall(context, transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, undefined, ...tbls))]; + return transformedExpressions + .slice(0, lastPrecedingStatementsIndex) + .filter((e, i) => shouldMoveToTemp(context, expressions[i], e)).length; } // Transforms a list of expressions while flattening spreads and maintaining execution order @@ -117,13 +144,47 @@ export function transformExpressionList( context: TransformationContext, expressions: readonly ts.Expression[] ): lua.Expression[] { - const [transformedExpressions, lastPrecedingStatementsIndex] = transformExpressionsInOrder(context, expressions); - - // If there are spreads in the middle, use the array concat lib function - const firstSpreadIndex = expressions.findIndex(i => ts.isSpreadElement(i)); - if (firstSpreadIndex >= 0 && firstSpreadIndex < expressions.length - 1) { - return buildArrayConcatCall(context, expressions, transformedExpressions, lastPrecedingStatementsIndex); + const [transformedExpressions, precedingStatements, lastPrecedingStatementsIndex] = transformExpressions( + context, + expressions + ); + + // If more than this number of temps are required to preserve execution order, we'll fall back to using the list + // lib functions instead to prevent excessive locals. + const maxTemps = 2; + + // Use list lib if there are spreads before the last expression or if too many temps are needed to preserve order + const lastSpread = expressions.findIndex(e => ts.isSpreadElement(e)); + if ( + (lastSpread >= 0 && lastSpread < expressions.length - 1) || + countTempCandidates(context, expressions, transformedExpressions, lastPrecedingStatementsIndex) > maxTemps + ) { + return transformExpressionsUsingList(context, expressions, transformedExpressions, precedingStatements); + } else { + return transformExpressionsUsingTemps( + context, + expressions, + transformedExpressions, + precedingStatements, + lastPrecedingStatementsIndex + ); } +} - return transformedExpressions; +// Transforms a series of expressions while maintaining execution order +export function transformOrderedExpressions( + context: TransformationContext, + expressions: readonly ts.Expression[] +): lua.Expression[] { + const [transformedExpressions, precedingStatements, lastPrecedingStatementsIndex] = transformExpressions( + context, + expressions + ); + return transformExpressionsUsingTemps( + context, + expressions, + transformedExpressions, + precedingStatements, + lastPrecedingStatementsIndex + ); } diff --git a/src/transformation/visitors/sourceFile.ts b/src/transformation/visitors/sourceFile.ts index 41561c67f..bd4c2c539 100644 --- a/src/transformation/visitors/sourceFile.ts +++ b/src/transformation/visitors/sourceFile.ts @@ -13,7 +13,10 @@ export const transformSourceFileNode: FunctionVisitor = (node, co const [statement] = node.statements; if (statement) { assert(ts.isExpressionStatement(statement)); - statements.push(lua.createReturnStatement([context.transformExpression(statement.expression)])); + context.pushPrecedingStatements(); + const expression = context.transformExpression(statement.expression); + statements.push(...context.popPrecedingStatements()); + statements.push(lua.createReturnStatement([expression])); } else { const errorCall = lua.createCallExpression(lua.createIdentifier("error"), [ lua.createStringLiteral("Unexpected end of JSON input"), diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index c18e08623..573f38639 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -75,9 +75,7 @@ describe("execution order", () => { function inc() { ++i; return i; } function foo(...args: unknown[]) { return args; } return foo(${sequence}); - ` - .debug() - .expectToMatchJsResult(); + `.expectToMatchJsResult(); }); test.each([ @@ -512,7 +510,7 @@ describe("loop expressions", () => { break; } } - return i; + return [i, j]; `.expectToMatchJsResult(); }); @@ -525,7 +523,7 @@ describe("loop expressions", () => { break; } } - return i; + return j; `.expectToMatchJsResult(); }); @@ -538,7 +536,7 @@ describe("loop expressions", () => { break; } } while (i++ < 5); - return i; + return [i, j]; `.expectToMatchJsResult(); }); From c936a055441cd40f593296d60070f6fb9a4fc99d Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 28 Sep 2021 14:03:22 -0600 Subject: [PATCH 30/48] addPrecedingStatements takes a single argument now --- src/transformation/context/context.ts | 5 ++++- src/transformation/utils/lua-ast.ts | 2 +- .../visitors/binary-expression/assignments.ts | 4 ++-- src/transformation/visitors/binary-expression/compound.ts | 2 +- src/transformation/visitors/call.ts | 6 +++--- src/transformation/visitors/conditional.ts | 2 +- src/transformation/visitors/expression-list.ts | 6 +++--- src/transformation/visitors/function.ts | 6 +++--- 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index aa2059a2a..f166d2e32 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -134,9 +134,12 @@ export class TransformationContext { return precedingStatements; } - public addPrecedingStatements(statements: lua.Statement[], prepend = false) { + public addPrecedingStatements(statements: lua.Statement | lua.Statement[], prepend = false) { const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; assert(precedingStatements); + if (!Array.isArray(statements)) { + statements = [statements]; + } if (prepend) { precedingStatements.unshift(...statements); } else { diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index f5f6f1bf7..8197ff5da 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -161,7 +161,7 @@ export function createLocalOrExportedOrGlobalDeclaration( // Split declaration and assignment of identifiers that reference themselves in their declaration. // Put declaration above preceding statements in case the identifier is referenced in those. const precedingDeclaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal); - context.addPrecedingStatements([precedingDeclaration], true); + context.addPrecedingStatements(precedingDeclaration, true); if (rhs) { assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); } diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index adc09378f..53f22b49a 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -29,14 +29,14 @@ export function transformAssignmentLeftHandSideExpression( // If table is complex, it could reference things from the index expression and needs to be cached as well if (!ts.isIdentifier(node.expression)) { const tableTemp = context.createTempForNode(node.expression); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(tableTemp, table, node.expression)]); + context.addPrecedingStatements(lua.createVariableDeclarationStatement(tableTemp, table, node.expression)); table = lua.cloneIdentifier(tableTemp); } const indexNode = node.argumentExpression; const indexTemp = context.createTempForNode(indexNode); const index = transformElementAccessArgument(context, node); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(indexTemp, index, indexNode)]); + context.addPrecedingStatements(lua.createVariableDeclarationStatement(indexTemp, index, indexNode)); return lua.createTableIndexExpression(table, lua.cloneIdentifier(indexTemp), node); } diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index 2f79df6a7..3b934c44f 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -175,7 +175,7 @@ export function transformCompoundAssignmentExpression( isPostfix: boolean ): lua.Expression { const { statements, result } = transformCompoundAssignment(context, expression, lhs, rhs, operator, isPostfix); - context.addPrecedingStatements(Array.isArray(statements) ? statements : [statements]); + context.addPrecedingStatements(statements); return result; } diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 1abe130db..36bf8b1ca 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -108,7 +108,7 @@ function transformElementAccessCall( const selfIdentifier = lua.createIdentifier(context.createTempName("self")); const callContext = context.transformExpression(left.expression); const selfAssignment = lua.createVariableDeclarationStatement(selfIdentifier, callContext); - context.addPrecedingStatements([selfAssignment]); + context.addPrecedingStatements(selfAssignment); const argument = ts.isElementAccessExpression(left) ? transformElementAccessArgument(context, left) @@ -256,7 +256,7 @@ export const transformCallExpression: FunctionVisitor = (node if (isTableDeleteCall(context, node)) { context.diagnostics.push(invalidTableDeleteExpression(node)); - context.addPrecedingStatements([transformTableDeleteExpression(context, node)]); + context.addPrecedingStatements(transformTableDeleteExpression(context, node)); return lua.createNilLiteral(); } @@ -270,7 +270,7 @@ export const transformCallExpression: FunctionVisitor = (node if (isTableSetCall(context, node)) { context.diagnostics.push(invalidTableSetExpression(node)); - context.addPrecedingStatements([transformTableSetExpression(context, node)]); + context.addPrecedingStatements(transformTableSetExpression(context, node)); return lua.createNilLiteral(); } diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts index 57b8780ab..1a6cf2ee3 100644 --- a/src/transformation/visitors/conditional.ts +++ b/src/transformation/visitors/conditional.ts @@ -45,8 +45,8 @@ function transformProtectedConditionalExpression( const falseStatements = context.popPrecedingStatements(); falseStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val2, expression.whenFalse)); - context.addPrecedingStatements([lua.createVariableDeclarationStatement(tempVar, undefined, expression.condition)]); context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(tempVar, undefined, expression.condition), lua.createIfStatement( condition, lua.createBlock(trueStatements, expression.whenTrue), diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 21b6710db..cf70e09f6 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -17,7 +17,7 @@ export function moveToPrecedingTemp( const tempIdentifier = context.createTempForLuaExpression(expression); const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression); lua.setNodePosition(tempDeclaration, lua.getOriginalPos(expression)); - context.addPrecedingStatements([tempDeclaration]); + context.addPrecedingStatements(tempDeclaration); const tempClone = lua.cloneIdentifier(tempIdentifier); lua.setNodePosition(tempClone, lua.getOriginalPos(tempIdentifier)); return tempClone; @@ -79,7 +79,7 @@ function pushToList( listIdentifier = lua.createIdentifier(context.createTempName("list")); const libCall = transformLuaLibFunction(context, LuaLibFeature.SparseArrayNew, undefined, ...expressions); const declaration = lua.createVariableDeclarationStatement(listIdentifier, libCall); - context.addPrecedingStatements([declaration]); + context.addPrecedingStatements(declaration); } else { const libCall = transformLuaLibFunction( context, @@ -88,7 +88,7 @@ function pushToList( listIdentifier, ...expressions ); - context.addPrecedingStatements([lua.createExpressionStatement(libCall)]); + context.addPrecedingStatements(lua.createExpressionStatement(libCall)); } return listIdentifier; } diff --git a/src/transformation/visitors/function.ts b/src/transformation/visitors/function.ts index 1ada847eb..3c250f74e 100644 --- a/src/transformation/visitors/function.ts +++ b/src/transformation/visitors/function.ts @@ -246,9 +246,9 @@ export function transformFunctionLikeDeclaration( // Only handle if the name is actually referenced inside the function if (isReferenced) { const nameIdentifier = transformIdentifier(context, node.name); - context.addPrecedingStatements([ - lua.createVariableDeclarationStatement(nameIdentifier, functionExpression), - ]); + context.addPrecedingStatements( + lua.createVariableDeclarationStatement(nameIdentifier, functionExpression) + ); return lua.cloneIdentifier(nameIdentifier); } } From 92afa7c7837a0ee342129d883e9fee43af4ec11d Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 28 Sep 2021 16:24:59 -0600 Subject: [PATCH 31/48] a little cleanup --- src/transformation/utils/scope.ts | 11 ----------- src/transformation/visitors/access.ts | 22 +++++++--------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index 02a8e31b2..3a4558b4d 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -67,17 +67,6 @@ export function markSymbolAsReferencedInCurrentScopes( } } -export function getReferenceCountInScope(scope: Scope, symbolId: lua.SymbolId) { - if (!scope.referencedSymbols) { - return 0; - } - const referencedSymbols = scope.referencedSymbols.get(symbolId); - if (!referencedSymbols) { - return 0; - } - return referencedSymbols.length; -} - export function peekScope(context: TransformationContext): Scope { const scopeStack = getScopeStack(context); const scope = scopeStack[scopeStack.length - 1]; diff --git a/src/transformation/visitors/access.ts b/src/transformation/visitors/access.ts index 2c15a58b1..7157d6c9e 100644 --- a/src/transformation/visitors/access.ts +++ b/src/transformation/visitors/access.ts @@ -11,33 +11,25 @@ import { tryGetConstEnumValue } from "./enum"; import { transformOrderedExpressions } from "./expression-list"; import { isMultiReturnCall, returnsMultiType } from "./language-extensions/multi"; -export function transformElementAccessArgument( +function getElementAccessArgument( context: TransformationContext, - node: ts.ElementAccessExpression + node: ts.ElementAccessExpression, + index: lua.Expression ): lua.Expression { - const index = context.transformExpression(node.argumentExpression); - 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 function getElementAccessArgument( +export function transformElementAccessArgument( context: TransformationContext, - node: ts.ElementAccessExpression, - index: lua.Expression + node: ts.ElementAccessExpression ): 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; + const index = context.transformExpression(node.argumentExpression); + return getElementAccessArgument(context, node, index); } export const transformElementAccessExpression: FunctionVisitor = (node, context) => { From a7d7b3e87cafc9ce7e2684b60225eb011f944040 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 28 Sep 2021 16:50:00 -0600 Subject: [PATCH 32/48] fix for indirect property assignments --- .../visitors/binary-expression/assignments.ts | 31 +++++++------------ test/unit/precedingStatements.spec.ts | 11 +++++++ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index 53f22b49a..6ecdd138e 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -11,33 +11,26 @@ 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"; +import { moveToPrecedingTemp, transformOrderedExpressions } from "../expression-list"; export function transformAssignmentLeftHandSideExpression( context: TransformationContext, node: ts.Expression, rightHasPrecedingStatements?: boolean ): lua.AssignmentLeftHandSideExpression { - // Cache index expression in a temp so it can be evaluated before right's preceding statements - if ( - rightHasPrecedingStatements && - ts.isElementAccessExpression(node) && - !ts.isLiteralExpression(node.argumentExpression) - ) { + // Access expressions need the components of the left side cached in temps before the right side's preceding statements + if (rightHasPrecedingStatements && (ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node))) { 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(node.expression)) { - const tableTemp = context.createTempForNode(node.expression); - context.addPrecedingStatements(lua.createVariableDeclarationStatement(tableTemp, table, node.expression)); - table = lua.cloneIdentifier(tableTemp); + table = moveToPrecedingTemp(context, table, node.expression); + + let index: lua.Expression; + if (ts.isElementAccessExpression(node)) { + index = transformElementAccessArgument(context, node); + index = moveToPrecedingTemp(context, index, node.argumentExpression); + } else { + index = lua.createStringLiteral(node.name.text, node.name); } - - const indexNode = node.argumentExpression; - const indexTemp = context.createTempForNode(indexNode); - const index = transformElementAccessArgument(context, node); - context.addPrecedingStatements(lua.createVariableDeclarationStatement(indexTemp, index, indexNode)); - return lua.createTableIndexExpression(table, lua.cloneIdentifier(indexTemp), node); + return lua.createTableIndexExpression(table, index, node); } const symbol = context.checker.getSymbolAtLocation(node); diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 573f38639..7831ee7da 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -191,6 +191,17 @@ describe("execution order", () => { `.expectToMatchJsResult(); }); + test("indirect property assignment expression", () => { + util.testFunction` + const a = {value: 10}; + const b = {value: 11}; + let i = 0; + function foo() { if (i === 0) { return b; } else { return a; } } + foo().value = i++; + return [a, b, i]; + `.expectToMatchJsResult(); + }); + test("compound index assignment statement", () => { util.testFunction` let i = 0; From 40c24a8d76826433325111e45a8c7cc6956ed892 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 28 Sep 2021 18:08:49 -0600 Subject: [PATCH 33/48] refactoring to expression-lists --- .../visitors/expression-list.ts | 79 +++++++++---------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index cf70e09f6..e2c9ae0be 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -5,13 +5,17 @@ import { TransformationContext } from "../context"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { isConstIdentifier } from "../utils/typescript"; +function shouldMoveToTemp(context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node) { + return !lua.isLiteral(expression) && !(tsOriginal && isConstIdentifier(context, tsOriginal)); +} + // Cache an expression in a preceding statement and return the temp identifier export function moveToPrecedingTemp( context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node ): lua.Expression { - if (lua.isLiteral(expression) || (tsOriginal && isConstIdentifier(context, tsOriginal))) { + if (!shouldMoveToTemp(context, expression, tsOriginal)) { return expression; } const tempIdentifier = context.createTempForLuaExpression(expression); @@ -23,14 +27,6 @@ export function moveToPrecedingTemp( return tempClone; } -function shouldMoveToTemp( - context: TransformationContext, - expression: ts.Expression, - transformedExpression: lua.Expression -) { - return !lua.isLiteral(transformedExpression) && !isConstIdentifier(context, expression); -} - function transformExpressions( context: TransformationContext, expressions: readonly ts.Expression[] @@ -59,73 +55,71 @@ function transformExpressionsUsingTemps( ) { for (let i = 0; i < transformedExpressions.length; ++i) { context.addPrecedingStatements(precedingStatements[i]); - - const transformedExpression = transformedExpressions[i]; - const expression = expressions[i]; - if (i < lastPrecedingStatementsIndex && shouldMoveToTemp(context, expression, transformedExpression)) { - transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpression, expression); + if (i < lastPrecedingStatementsIndex) { + transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpressions[i], expressions[i]); } } - return transformedExpressions; } -function pushToList( +function pushToSparseArray( context: TransformationContext, - listIdentifier: lua.Identifier | undefined, + arrayIdentifier: lua.Identifier | undefined, expressions: lua.Expression[] ) { - if (!listIdentifier) { - listIdentifier = lua.createIdentifier(context.createTempName("list")); + if (!arrayIdentifier) { + arrayIdentifier = lua.createIdentifier(context.createTempName("list")); const libCall = transformLuaLibFunction(context, LuaLibFeature.SparseArrayNew, undefined, ...expressions); - const declaration = lua.createVariableDeclarationStatement(listIdentifier, libCall); + const declaration = lua.createVariableDeclarationStatement(arrayIdentifier, libCall); context.addPrecedingStatements(declaration); } else { const libCall = transformLuaLibFunction( context, LuaLibFeature.SparseArrayPush, undefined, - listIdentifier, + arrayIdentifier, ...expressions ); context.addPrecedingStatements(lua.createExpressionStatement(libCall)); } - return listIdentifier; + return arrayIdentifier; } -function transformExpressionsUsingList( +function transformExpressionsUsingSparseArray( context: TransformationContext, expressions: readonly ts.Expression[], transformedExpressions: lua.Expression[], precedingStatements: lua.Statement[][] ) { - let listIdentifier: lua.Identifier | undefined; + let arrayIdentifier: lua.Identifier | undefined; - let expressionSet: lua.Expression[] = []; + let expressionBatch: lua.Expression[] = []; for (let i = 0; i < expressions.length; ++i) { - if (precedingStatements[i].length > 0 && expressionSet.length > 0) { - listIdentifier = pushToList(context, listIdentifier, expressionSet); - expressionSet = []; + // Expressions with preceding statements should always be at the start of a batch + if (precedingStatements[i].length > 0 && expressionBatch.length > 0) { + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); + expressionBatch = []; } context.addPrecedingStatements(precedingStatements[i]); - expressionSet.push(transformedExpressions[i]); + expressionBatch.push(transformedExpressions[i]); + // Spread expressions should always be at the end of a batch if (ts.isSpreadElement(expressions[i])) { - listIdentifier = pushToList(context, listIdentifier, expressionSet); - expressionSet = []; + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); + expressionBatch = []; } } - if (expressionSet.length > 0) { - listIdentifier = pushToList(context, listIdentifier, expressionSet); + if (expressionBatch.length > 0) { + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); } - assert(listIdentifier); - return [transformLuaLibFunction(context, LuaLibFeature.SparseArraySpread, undefined, listIdentifier)]; + assert(arrayIdentifier); + return [transformLuaLibFunction(context, LuaLibFeature.SparseArraySpread, undefined, arrayIdentifier)]; } -function countTempCandidates( +function countNeededTemps( context: TransformationContext, expressions: readonly ts.Expression[], transformedExpressions: lua.Expression[], @@ -136,7 +130,7 @@ function countTempCandidates( } return transformedExpressions .slice(0, lastPrecedingStatementsIndex) - .filter((e, i) => shouldMoveToTemp(context, expressions[i], e)).length; + .filter((e, i) => shouldMoveToTemp(context, e, expressions[i])).length; } // Transforms a list of expressions while flattening spreads and maintaining execution order @@ -149,17 +143,18 @@ export function transformExpressionList( expressions ); - // If more than this number of temps are required to preserve execution order, we'll fall back to using the list - // lib functions instead to prevent excessive locals. + // If more than this number of temps are required to preserve execution order, we'll fall back to using the + // sparse array lib functions instead to prevent excessive locals. const maxTemps = 2; - // Use list lib if there are spreads before the last expression or if too many temps are needed to preserve order + // Use sparse array lib if there are spreads before the last expression + // or if too many temps are needed to preserve order const lastSpread = expressions.findIndex(e => ts.isSpreadElement(e)); if ( (lastSpread >= 0 && lastSpread < expressions.length - 1) || - countTempCandidates(context, expressions, transformedExpressions, lastPrecedingStatementsIndex) > maxTemps + countNeededTemps(context, expressions, transformedExpressions, lastPrecedingStatementsIndex) > maxTemps ) { - return transformExpressionsUsingList(context, expressions, transformedExpressions, precedingStatements); + return transformExpressionsUsingSparseArray(context, expressions, transformedExpressions, precedingStatements); } else { return transformExpressionsUsingTemps( context, From ccb1c2618ae65e3bcab054aa163325b357c139b7 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 29 Sep 2021 06:01:58 -0600 Subject: [PATCH 34/48] fixes and refactors to tests --- test/unit/precedingStatements.spec.ts | 131 ++++++++++++++------------ 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 7831ee7da..2e79bfc7e 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -55,8 +55,8 @@ describe("execution order", () => { "i, ...a, i++, ...[1, i++, 2], i, i++, ...a", "i, inc(), i++", "i, ...[1, i++, inc(), 2], i++", - "[i, ...'foo', i++]", - "[i, ...([1, i++, 2] as any), i++]", + "i, ...'foo', i++", + "i, ...([1, i++, 2] as any), i++", ]; test.each(sequenceTests)("array literal ([%p])", sequence => { @@ -150,7 +150,9 @@ describe("execution order", () => { return [result, i]; `.expectToMatchJsResult(); }); +}); +describe("assignment execution order", () => { test("index assignment statement", () => { util.testFunction` let i = 0; @@ -174,7 +176,7 @@ describe("execution order", () => { let i = 0; const a = [9, 8]; const b = [7, 6]; - function foo(x: number) { if (x > 0) { return a; } else { return b; } } + function foo(x: number) { return (x > 0) ? b : a; } foo(i)[i] = i++; return [a, b, i]; `.expectToMatchJsResult(); @@ -185,23 +187,34 @@ describe("execution order", () => { let i = 0; const a = [9, 8]; const b = [7, 6]; - function foo(x: number) { if (x > 0) { return a; } else { return b; } } + function foo(x: number) { return (x > 0) ? b : a; } const result = foo(i)[i] = i++; return [result, a, b, i]; `.expectToMatchJsResult(); }); - test("indirect property assignment expression", () => { + test("indirect property assignment statement", () => { util.testFunction` const a = {value: 10}; const b = {value: 11}; let i = 0; - function foo() { if (i === 0) { return b; } else { return a; } } - foo().value = i++; + function foo(x: number) { return (x > 0) ? b : a; } + foo(i).value = i++; return [a, b, i]; `.expectToMatchJsResult(); }); + test("indirect property assignment expression", () => { + util.testFunction` + const a = {value: 10}; + const b = {value: 11}; + let i = 0; + function foo(x: number) { return (x > 0) ? b : a; } + const result = foo(i).value = i++; + return [result, a, b, i]; + `.expectToMatchJsResult(); + }); + test("compound index assignment statement", () => { util.testFunction` let i = 0; @@ -225,7 +238,7 @@ describe("execution order", () => { let i = 0; const a = [9, 8]; const b = [7, 6]; - function foo(x: number) { if (x > 0) { return a; } else { return b; } } + function foo(x: number) { return (x > 0) ? b : a; } foo(i)[i] += i++; return [a, b, i]; `.expectToMatchJsResult(); @@ -236,7 +249,7 @@ describe("execution order", () => { let i = 1; const a = [9, 8]; const b = [7, 6]; - function foo(x: number) { if (x > 0) { return a; } else { return b; } } + function foo(x: number) { return (x > 0) ? b : a; } const result = foo(i)[i] += i++; return [result, a, b, i]; `.expectToMatchJsResult(); @@ -255,8 +268,8 @@ describe("execution order", () => { 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]; + const result = [a[i], a[i++]] = [i++, i++]; + return [a, i, result]; `.expectToMatchJsResult(); }); @@ -273,8 +286,8 @@ describe("execution order", () => { util.testFunction` const a = [10, 9, 8, 7, 6, 5]; let i = 0; - const x = [a[i] = i++, a[i++]] = [i++, i++]; - return [a, i, x]; + const result = [a[i] = i++, a[i++]] = [i++, i++]; + return [a, i, result]; `.expectToMatchJsResult(); }); @@ -291,81 +304,81 @@ describe("execution order", () => { 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]; + const result = [a[0][i], ...a[i++]] = [i++, i++]; + return [a, i, result]; `.expectToMatchJsResult(); }); test("object destructuring assignment statement", () => { util.testFunction` - let i = "A"; + let s = "A"; const o: Record = {ABCDEFG: "success", result: ""}; - function getO(x: string) { i = x + "C"; return o; } - function getO2(x: string) { i = x + "G"; return o; } - function getI(x: string) { i = x + "E"; return i; } - ({ [getI(i += "D")]: getO2(i += "F").result } = getO(i += "B")); - return [i, o]; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function e(x: string) { s = x + "E"; return s; } + ({ [e(s += "D")]: g(s += "F").result } = c(s += "B")); + return [s, o]; `.expectToMatchJsResult(); }); test("object destructuring assignment statement with default", () => { util.testFunction` - let i = "A"; + let s = "A"; const o: Record = {ABCDEFGHIJ: "success", result: ""}; - function getO(x: string) { i = x + "C"; return o; } - function getO2(x: string) { i = x + "G"; return o; } - function getO3(x: string) { i = x + "I"; return o; } - function getI(x: string): any { i = x + "E"; return undefined; } - ({ [getI(i += "D")]: getO2(i += "F").result = getO3(i += "H")[i += "J"] } = getO(i += "B")); - return [o, i]; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function i(x: string) { s = x + "I"; return o; } + function e(x: string): any { s = x + "E"; return undefined; } + ({ [e(s += "D")]: g(s += "F").result = i(s += "H")[s += "J"] } = c(s += "B")); + return [o, s]; `.expectToMatchJsResult(); }); test("object destructuring assignment expression", () => { util.testFunction` - let i = "A"; + let s = "A"; const o: Record = {ABCDEFG: "success", result: ""}; - function getO(x: string) { i = x + "C"; return o; } - function getO2(x: string) { i = x + "G"; return o; } - function getI(x: string) { i = x + "E"; return i; } - const x = ({ [getI(i += "D")]: getO2(i += "F").result } = getO(i += "B")); - return [i, o, x]; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function e(x: string) { s = x + "E"; return s; } + const result = ({ [e(s += "D")]: g(s += "F").result } = c(s += "B")); + return [s, o, result]; `.expectToMatchJsResult(); }); test("object destructuring assignment expression with default", () => { util.testFunction` - let i = "A"; + let s = "A"; const o: Record = {ABCDEFGHIJ: "success", result: ""}; - function getO(x: string) { i = x + "C"; return o; } - function getO2(x: string) { i = x + "G"; return o; } - function getO3(x: string) { i = x + "I"; return o; } - function getI(x: string): any { i = x + "E"; return undefined; } - const x = ({ [getI(i += "D")]: getO2(i += "F").result = getO3(i += "H")[i += "J"] } = getO(i += "B")); - return [o, i, x]; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function i(x: string) { s = x + "I"; return o; } + function e(x: string): any { s = x + "E"; return undefined; } + const result = ({ [e(s += "D")]: g(s += "F").result = i(s += "H")[s += "J"] } = c(s += "B")); + return [o, s, result]; `.expectToMatchJsResult(); }); test("object destructuring declaration", () => { util.testFunction` - let i = "A"; + let s = "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]; + function c(x: string) { s = x + "C"; return o; } + function e(x: string) { s = x + "E"; return s; } + const { [e(s += "D")]: result } = c(s += "B"); + return [result, s]; `.expectToMatchJsResult(); }); test("object destructuring declaration with default", () => { util.testFunction` - let i = "A"; + let s = "A"; const o: Record = {ABCDEFGH: "success"}; - function getO(x: string) { i = x + "C"; return o; } - function getO2(x: string) { i = x + "G"; return o; } - function getI(x: string): any { i = x + "E"; return undefined; } - const { [getI(i += "D")]: result = getO2(i += "F")[i += "H"]} = getO(i += "B"); - return [result, i]; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function e(x: string): any { s = x + "E"; return undefined; } + const { [e(s += "D")]: result = g(s += "F")[s += "H"]} = c(s += "B"); + return [result, s]; `.expectToMatchJsResult(); }); @@ -374,7 +387,7 @@ describe("execution order", () => { let i = 1; 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; } } + function foo(x: number) { return (x > 0) ? b : a; } const result = foo(i)(i++); return [result, i]; `.expectToMatchJsResult(); @@ -384,11 +397,11 @@ describe("execution order", () => { util.testFunction` let i = 1; let foo = (x: null, y: number) => { return y; }; - function bar() { + function changeFoo() { foo = (x: null, y: number) => { return y * 10; }; return null; } - const result = foo(bar(), i++); + const result = foo(changeFoo(), i++); return [result, i]; `.expectToMatchJsResult(); }); @@ -471,7 +484,7 @@ describe("execution order", () => { util.testFunction` let a = [7]; let b = [9]; - function foo(x: number) { if (x > 0) { return b; } else { return a; } } + function foo(x: number) { return (x > 0) ? b : a; } let i = 0; foo(i).push(i, i++, i); return [a, b, i]; @@ -483,7 +496,7 @@ describe("execution order", () => { 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; } } + function foo(x: number) { return (x > 0) ? b : a; } let i = 0; const result = foo(i).call(o, i++); return [result, i]; @@ -492,7 +505,7 @@ describe("execution order", () => { test("string method call", () => { util.testFunction` - function foo(x: number) { if (x > 0) { return "foo"; } else { return "bar"; } } + function foo(x: number) { return (x > 0) ? "foo" : "bar"; } let i = 0; const result = foo(i).substr(++i); return [result, i]; @@ -503,7 +516,7 @@ describe("execution order", () => { 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; } } + function foo(x: number) { return (x > 0) ? B : A; } let i = 0; const result = new (foo(i))(i++).val; return [result, i]; From 09ae80bc2372b921bd383fee36e413c9e07a863f Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 29 Sep 2021 07:07:06 -0600 Subject: [PATCH 35/48] improvements to temp variable naming --- src/transformation/context/context.ts | 48 ++++++++++++++++--- .../visitors/expression-list.ts | 2 +- .../__snapshots__/multi.spec.ts.snap | 8 ++-- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index f166d2e32..d2c9ef36a 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -151,29 +151,63 @@ export class TransformationContext { return `____${prefix}_${this.nextTempId++}`; } - public createTempForLuaExpression(expression: lua.Expression) { + private getTempNameForLuaExpression(expression: lua.Expression): string | undefined { let name: string | undefined; - if (lua.isStringLiteral(expression)) { - name = expression.value; + if (lua.isStringLiteral(expression) || lua.isNumericLiteral(expression)) { + name = expression.value.toString(); } else if (lua.isIdentifier(expression)) { name = expression.text; + } else if (lua.isCallExpression(expression)) { + name = this.getTempNameForLuaExpression(expression.expression); + if (name) { + name = `${name}_result`; + } + } else if (lua.isTableIndexExpression(expression)) { + const tableName = this.getTempNameForLuaExpression(expression.table); + const indexName = this.getTempNameForLuaExpression(expression.index); + if (tableName || indexName) { + name = `${tableName ?? "table"}_${indexName ?? "index"}`; + } } if (name && !isValidLuaIdentifier(name)) { name = fixInvalidLuaIdentifier(name); } + return name; + } + + public createTempForLuaExpression(expression: lua.Expression) { + const name = this.getTempNameForLuaExpression(expression); const identifier = lua.createIdentifier(this.createTempName(name)); lua.setNodePosition(identifier, lua.getOriginalPos(expression)); return identifier; } - public createTempForNode(node: ts.Node) { + private getTempNameForNode(node: ts.Node): string | undefined { let name: string | undefined; - if (ts.isStringLiteral(node) || ts.isIdentifier(node) || ts.isMemberName(node)) { + if (ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isIdentifier(node) || ts.isMemberName(node)) { name = node.text; - if (!isValidLuaIdentifier(name)) { - name = fixInvalidLuaIdentifier(name); + } else if (ts.isCallExpression(node)) { + name = this.getTempNameForNode(node.expression); + if (name) { + name = `${name}_result`; } + } else if (ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node)) { + const tableName = this.getTempNameForNode(node.expression); + const indexName = ts.isElementAccessExpression(node) + ? this.getTempNameForNode(node.argumentExpression) + : node.name.text; + if (tableName || indexName) { + name = `${tableName ?? "table"}_${indexName ?? "index"}`; + } + } + if (name && !isValidLuaIdentifier(name)) { + name = fixInvalidLuaIdentifier(name); } + return name; + } + + public createTempForNode(node: ts.Node) { + const name = this.getTempNameForNode(node); return lua.createIdentifier(this.createTempName(name), node); } } diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index e2c9ae0be..3d79980da 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -68,7 +68,7 @@ function pushToSparseArray( expressions: lua.Expression[] ) { if (!arrayIdentifier) { - arrayIdentifier = lua.createIdentifier(context.createTempName("list")); + arrayIdentifier = lua.createIdentifier(context.createTempName("array")); const libCall = transformLuaLibFunction(context, LuaLibFeature.SparseArrayNew, undefined, ...expressions); const declaration = lua.createVariableDeclarationStatement(arrayIdentifier, libCall); context.addPrecedingStatements(declaration); diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index 908685c6b..e3f15be29 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -66,7 +66,7 @@ end" exports[`invalid $multi call (const [a = 0] = $multi()): diagnostics 1`] = `"main.ts(2,25): error TSTL: The $multi function must be called in a return statement."`; exports[`invalid $multi call (const {} = $multi();): code 1`] = ` -"local ____temp_0 = { +"local _____24multi_result_0 = { { ____(_G) } @@ -237,12 +237,12 @@ local function multi(self, ...) return ... end local a -local ____temp_0 = { +local _____24multi_result_0 = { ____(nil, 1) } -a = ____temp_0[1] +a = _____24multi_result_0[1] ____exports.a = a -if ____temp_0 then +if _____24multi_result_0 then a = a + 1 ____exports.a = a end From 309f90af9181b1164d6aada279f0df065d3db8aa Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 29 Sep 2021 07:38:53 -0600 Subject: [PATCH 36/48] treating temps like consts and more temp name adjustments --- src/transformation/context/context.ts | 37 ++++++++----------- src/transformation/utils/symbols.ts | 2 + .../visitors/expression-list.ts | 7 +++- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index d2c9ef36a..316e11db5 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -5,6 +5,7 @@ import { assert, castArray } from "../../utils"; import { unsupportedNodeKind } from "../utils/diagnostics"; import { unwrapVisitorResult } from "../utils/lua-ast"; import { fixInvalidLuaIdentifier, isValidLuaIdentifier } from "../utils/safe-names"; +import { tempSymbolId } from "../utils/symbols"; import { ExpressionLikeNode, ObjectVisitor, StatementLikeNode, VisitorMap } from "./visitors"; export interface AllAccessorDeclarations { @@ -148,48 +149,46 @@ export class TransformationContext { } public createTempName(prefix = "temp") { + if (!isValidLuaIdentifier(prefix)) { + prefix = fixInvalidLuaIdentifier(prefix); + } + prefix = prefix.replace(/^_*/, ""); return `____${prefix}_${this.nextTempId++}`; } private getTempNameForLuaExpression(expression: lua.Expression): string | undefined { - let name: string | undefined; if (lua.isStringLiteral(expression) || lua.isNumericLiteral(expression)) { - name = expression.value.toString(); + return expression.value.toString(); } else if (lua.isIdentifier(expression)) { - name = expression.text; + return expression.text; } else if (lua.isCallExpression(expression)) { - name = this.getTempNameForLuaExpression(expression.expression); + const name = this.getTempNameForLuaExpression(expression.expression); if (name) { - name = `${name}_result`; + return `${name}_result`; } } else if (lua.isTableIndexExpression(expression)) { const tableName = this.getTempNameForLuaExpression(expression.table); const indexName = this.getTempNameForLuaExpression(expression.index); if (tableName || indexName) { - name = `${tableName ?? "table"}_${indexName ?? "index"}`; + return `${tableName ?? "table"}_${indexName ?? "index"}`; } } - if (name && !isValidLuaIdentifier(name)) { - name = fixInvalidLuaIdentifier(name); - } - return name; } public createTempForLuaExpression(expression: lua.Expression) { const name = this.getTempNameForLuaExpression(expression); - const identifier = lua.createIdentifier(this.createTempName(name)); + const identifier = lua.createIdentifier(this.createTempName(name), undefined, tempSymbolId); lua.setNodePosition(identifier, lua.getOriginalPos(expression)); return identifier; } private getTempNameForNode(node: ts.Node): string | undefined { - let name: string | undefined; if (ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isIdentifier(node) || ts.isMemberName(node)) { - name = node.text; + return node.text; } else if (ts.isCallExpression(node)) { - name = this.getTempNameForNode(node.expression); + const name = this.getTempNameForNode(node.expression); if (name) { - name = `${name}_result`; + return `${name}_result`; } } else if (ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node)) { const tableName = this.getTempNameForNode(node.expression); @@ -197,17 +196,13 @@ export class TransformationContext { ? this.getTempNameForNode(node.argumentExpression) : node.name.text; if (tableName || indexName) { - name = `${tableName ?? "table"}_${indexName ?? "index"}`; + return `${tableName ?? "table"}_${indexName ?? "index"}`; } } - if (name && !isValidLuaIdentifier(name)) { - name = fixInvalidLuaIdentifier(name); - } - return name; } public createTempForNode(node: ts.Node) { const name = this.getTempNameForNode(node); - return lua.createIdentifier(this.createTempName(name), node); + return lua.createIdentifier(this.createTempName(name), node, tempSymbolId); } } diff --git a/src/transformation/utils/symbols.ts b/src/transformation/utils/symbols.ts index f800eea86..f6afdf8e6 100644 --- a/src/transformation/utils/symbols.ts +++ b/src/transformation/utils/symbols.ts @@ -5,6 +5,8 @@ import { TransformationContext } from "../context"; import { isOptimizedVarArgSpread } from "../visitors/spread"; import { markSymbolAsReferencedInCurrentScopes } from "./scope"; +export const tempSymbolId = -1 as lua.SymbolId; + const symbolIdCounters = new WeakMap(); function nextSymbolId(context: TransformationContext): lua.SymbolId { const symbolId = (symbolIdCounters.get(context) ?? 0) + 1; diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 3d79980da..a68703181 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -3,10 +3,15 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { tempSymbolId } from "../utils/symbols"; import { isConstIdentifier } from "../utils/typescript"; function shouldMoveToTemp(context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node) { - return !lua.isLiteral(expression) && !(tsOriginal && isConstIdentifier(context, tsOriginal)); + return ( + !lua.isLiteral(expression) && + !(lua.isIdentifier(expression) && expression.symbolId === tempSymbolId) && + !(tsOriginal && isConstIdentifier(context, tsOriginal)) + ); } // Cache an expression in a preceding statement and return the temp identifier From 6aae347a75bda029972b862357db1dd5447b0a8e Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 29 Sep 2021 07:53:09 -0600 Subject: [PATCH 37/48] snapshot update --- .../language-extensions/__snapshots__/multi.spec.ts.snap | 8 ++++---- .../language-extensions/__snapshots__/table.spec.ts.snap | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index e3f15be29..4671519aa 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -66,7 +66,7 @@ end" exports[`invalid $multi call (const [a = 0] = $multi()): diagnostics 1`] = `"main.ts(2,25): error TSTL: The $multi function must be called in a return statement."`; exports[`invalid $multi call (const {} = $multi();): code 1`] = ` -"local _____24multi_result_0 = { +"local ____24multi_result_0 = { { ____(_G) } @@ -237,12 +237,12 @@ local function multi(self, ...) return ... end local a -local _____24multi_result_0 = { +local ____24multi_result_0 = { ____(nil, 1) } -a = _____24multi_result_0[1] +a = ____24multi_result_0[1] ____exports.a = a -if _____24multi_result_0 then +if ____24multi_result_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 31daea00d..211b69e6b 100644 --- a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap @@ -69,9 +69,9 @@ exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("con exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("declare function foo(arg: any): void; foo(tableDelete({}, \\"foo\\"));"): code 1`] = ` "local ____foo_1 = foo -local _____G_0 = _G; +local ____G_0 = _G; ({}).foo = nil -____foo_1(_____G_0, nil)" +____foo_1(____G_0, 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."`; @@ -99,9 +99,9 @@ exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as express exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("declare function foo(arg: any): void; foo(setTable({}, \\"foo\\", 3));"): code 1`] = ` "local ____foo_1 = foo -local _____G_0 = _G; +local ____G_0 = _G; ({}).foo = 3 -____foo_1(_____G_0, nil)" +____foo_1(____G_0, 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 ca0a27d7829bb28f83526e51d0337089c3282181 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Sep 2021 06:48:59 -0600 Subject: [PATCH 38/48] a couple of small refactorings --- src/transformation/context/context.ts | 9 +++------ src/transformation/utils/safe-names.ts | 2 +- .../visitors/binary-expression/assignments.ts | 4 ++-- src/transformation/visitors/expression-list.ts | 2 +- .../language-extensions/__snapshots__/multi.spec.ts.snap | 8 ++++---- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index 316e11db5..a0d72254b 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -4,7 +4,7 @@ import * as lua from "../../LuaAST"; import { assert, castArray } from "../../utils"; import { unsupportedNodeKind } from "../utils/diagnostics"; import { unwrapVisitorResult } from "../utils/lua-ast"; -import { fixInvalidLuaIdentifier, isValidLuaIdentifier } from "../utils/safe-names"; +import { createSafeName } from "../utils/safe-names"; import { tempSymbolId } from "../utils/symbols"; import { ExpressionLikeNode, ObjectVisitor, StatementLikeNode, VisitorMap } from "./visitors"; @@ -149,11 +149,8 @@ export class TransformationContext { } public createTempName(prefix = "temp") { - if (!isValidLuaIdentifier(prefix)) { - prefix = fixInvalidLuaIdentifier(prefix); - } - prefix = prefix.replace(/^_*/, ""); - return `____${prefix}_${this.nextTempId++}`; + prefix = prefix.replace(/^_*/, ""); // Strip leading underscores + return createSafeName(`${prefix}_${this.nextTempId++}`); } private getTempNameForLuaExpression(expression: lua.Expression): string | undefined { diff --git a/src/transformation/utils/safe-names.ts b/src/transformation/utils/safe-names.ts index b458930fc..877196ba4 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); } -export const fixInvalidLuaIdentifier = (name: string) => +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/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index 6ecdd138e..5d1bf6d71 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -11,7 +11,7 @@ import { isArrayLength, transformDestructuringAssignment } from "./destructuring import { isMultiReturnCall } from "../language-extensions/multi"; import { notAllowedOptionalAssignment } from "../../utils/diagnostics"; import { transformElementAccessArgument } from "../access"; -import { moveToPrecedingTemp, transformOrderedExpressions } from "../expression-list"; +import { moveToPrecedingTemp, transformExpressionList } from "../expression-list"; export function transformAssignmentLeftHandSideExpression( context: TransformationContext, @@ -218,7 +218,7 @@ export function transformAssignmentStatement( let right: lua.Expression | lua.Expression[]; if (ts.isArrayLiteralExpression(expression.right)) { - right = transformOrderedExpressions(context, expression.right.elements); + right = transformExpressionList(context, expression.right.elements); } else { right = context.transformExpression(expression.right); if (!isMultiReturnCall(context, expression.right) && isArrayType(context, rightType)) { diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index a68703181..4722c4a8d 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -9,7 +9,7 @@ import { isConstIdentifier } from "../utils/typescript"; function shouldMoveToTemp(context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node) { return ( !lua.isLiteral(expression) && - !(lua.isIdentifier(expression) && expression.symbolId === tempSymbolId) && + !(lua.isIdentifier(expression) && expression.symbolId === tempSymbolId) && // Treat generated temps as consts !(tsOriginal && isConstIdentifier(context, tsOriginal)) ); } diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index 4671519aa..e3f15be29 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -66,7 +66,7 @@ end" exports[`invalid $multi call (const [a = 0] = $multi()): diagnostics 1`] = `"main.ts(2,25): error TSTL: The $multi function must be called in a return statement."`; exports[`invalid $multi call (const {} = $multi();): code 1`] = ` -"local ____24multi_result_0 = { +"local _____24multi_result_0 = { { ____(_G) } @@ -237,12 +237,12 @@ local function multi(self, ...) return ... end local a -local ____24multi_result_0 = { +local _____24multi_result_0 = { ____(nil, 1) } -a = ____24multi_result_0[1] +a = _____24multi_result_0[1] ____exports.a = a -if ____24multi_result_0 then +if _____24multi_result_0 then a = a + 1 ____exports.a = a end From 8a7d589ca8f2f12f175eec68233fd932246c19f7 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 7 Nov 2021 08:16:35 -0700 Subject: [PATCH 39/48] refactoring based on comments & suggestions --- src/lualib/SparseArrayNew.ts | 11 ++- src/lualib/SparseArrayPush.ts | 8 +- src/lualib/SparseArraySpread.ts | 4 +- src/transformation/context/context.ts | 34 +++++--- src/transformation/utils/lua-ast.ts | 2 +- .../utils/preceding-statements.ts | 11 +++ src/transformation/utils/symbols.ts | 2 - src/transformation/visitors/access.ts | 20 +++-- .../visitors/binary-expression/assignments.ts | 39 +++++----- .../visitors/binary-expression/compound.ts | 27 +++---- .../destructuring-assignments.ts | 78 ++++++++++--------- .../visitors/binary-expression/index.ts | 16 ++-- src/transformation/visitors/call.ts | 13 ++-- src/transformation/visitors/conditional.ts | 30 ++++--- .../visitors/expression-list.ts | 5 +- src/transformation/visitors/function.ts | 7 +- src/transformation/visitors/loops/do-while.ts | 18 +++-- src/transformation/visitors/loops/for.ts | 11 ++- src/transformation/visitors/sourceFile.ts | 8 +- .../visitors/variable-declaration.ts | 18 +++-- 20 files changed, 211 insertions(+), 151 deletions(-) create mode 100644 src/transformation/utils/preceding-statements.ts diff --git a/src/lualib/SparseArrayNew.ts b/src/lualib/SparseArrayNew.ts index d7f8d9c8d..2370871ca 100644 --- a/src/lualib/SparseArrayNew.ts +++ b/src/lualib/SparseArrayNew.ts @@ -1,6 +1,9 @@ -type __TS__SparseArray = unknown[] & { "#": number }; +type __TS__SparseArray = T[] & { sparseLength: number }; -function __TS__SparseArrayNew(this: void, ...args: unknown[]) { - (args as __TS__SparseArray)["#"] = select("#", ...args); - return args; +function __TS__SparseArrayNew(this: void, ...args: T[]): __TS__SparseArray { + const sparseArray = [...args] as __TS__SparseArray; + // select("#", ...) counts the number of args passed, including nils. + // Note that we're depending on vararg optimization to occur here. + sparseArray.sparseLength = select("#", ...args); + return sparseArray; } diff --git a/src/lualib/SparseArrayPush.ts b/src/lualib/SparseArrayPush.ts index e077432c6..30704620f 100644 --- a/src/lualib/SparseArrayPush.ts +++ b/src/lualib/SparseArrayPush.ts @@ -1,8 +1,8 @@ -function __TS__SparseArrayPush(this: void, list: unknown[], ...args: unknown[]) { +function __TS__SparseArrayPush(this: void, sparseArray: __TS__SparseArray, ...args: T[]): void { const argsLen = select("#", ...args); - const listLen = (list as __TS__SparseArray)["#"]; + const listLen = sparseArray.sparseLength; for (const i of $range(1, argsLen)) { - list[listLen + i - 1] = args[i - 1]; + sparseArray[listLen + i - 1] = args[i - 1]; } - (list as __TS__SparseArray)["#"] = listLen + argsLen; + sparseArray.sparseLength = listLen + argsLen; } diff --git a/src/lualib/SparseArraySpread.ts b/src/lualib/SparseArraySpread.ts index 08a23b7d3..e365f19bc 100644 --- a/src/lualib/SparseArraySpread.ts +++ b/src/lualib/SparseArraySpread.ts @@ -1,4 +1,4 @@ -function __TS__SparseArraySpread(this: void, list: unknown[]) { +function __TS__SparseArraySpread(this: void, sparseArray: __TS__SparseArray): LuaMultiReturn { const _unpack = unpack ?? table.unpack; - return _unpack(list, 1, (list as __TS__SparseArray)["#"]); + return _unpack(sparseArray, 1, sparseArray.sparseLength); } diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index a0d72254b..30f274c03 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -5,9 +5,10 @@ import { assert, castArray } from "../../utils"; import { unsupportedNodeKind } from "../utils/diagnostics"; import { unwrapVisitorResult } from "../utils/lua-ast"; import { createSafeName } from "../utils/safe-names"; -import { tempSymbolId } from "../utils/symbols"; import { ExpressionLikeNode, ObjectVisitor, StatementLikeNode, VisitorMap } from "./visitors"; +export const tempSymbolId = -1 as lua.SymbolId; + export interface AllAccessorDeclarations { firstAccessor: ts.AccessorDeclaration; secondAccessor: ts.AccessorDeclaration | undefined; @@ -135,27 +136,34 @@ export class TransformationContext { return precedingStatements; } - public addPrecedingStatements(statements: lua.Statement | lua.Statement[], prepend = false) { + public addPrecedingStatements(statements: lua.Statement | lua.Statement[]) { const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; assert(precedingStatements); if (!Array.isArray(statements)) { statements = [statements]; } - if (prepend) { - precedingStatements.unshift(...statements); - } else { - precedingStatements.push(...statements); + precedingStatements.push(...statements); + } + + public prependPrecedingStatements(statements: lua.Statement | lua.Statement[]) { + const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; + assert(precedingStatements); + if (!Array.isArray(statements)) { + statements = [statements]; } + precedingStatements.unshift(...statements); } public createTempName(prefix = "temp") { - prefix = prefix.replace(/^_*/, ""); // Strip leading underscores + prefix = prefix.replace(/^_*/, ""); // Strip leading underscores because createSafeName will add them again return createSafeName(`${prefix}_${this.nextTempId++}`); } private getTempNameForLuaExpression(expression: lua.Expression): string | undefined { - if (lua.isStringLiteral(expression) || lua.isNumericLiteral(expression)) { - return expression.value.toString(); + if (lua.isStringLiteral(expression)) { + return expression.value; + } else if (lua.isNumericLiteral(expression)) { + return `_${expression.value.toString()}`; } else if (lua.isIdentifier(expression)) { return expression.text; } else if (lua.isCallExpression(expression)) { @@ -172,7 +180,7 @@ export class TransformationContext { } } - public createTempForLuaExpression(expression: lua.Expression) { + public createTempNameForLuaExpression(expression: lua.Expression) { const name = this.getTempNameForLuaExpression(expression); const identifier = lua.createIdentifier(this.createTempName(name), undefined, tempSymbolId); lua.setNodePosition(identifier, lua.getOriginalPos(expression)); @@ -180,8 +188,10 @@ export class TransformationContext { } private getTempNameForNode(node: ts.Node): string | undefined { - if (ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isIdentifier(node) || ts.isMemberName(node)) { + if (ts.isStringLiteral(node) || ts.isIdentifier(node) || ts.isMemberName(node)) { return node.text; + } else if (ts.isNumericLiteral(node)) { + return `_${node.text}`; } else if (ts.isCallExpression(node)) { const name = this.getTempNameForNode(node.expression); if (name) { @@ -198,7 +208,7 @@ export class TransformationContext { } } - public createTempForNode(node: ts.Node) { + public createTempNameForNode(node: ts.Node) { const name = this.getTempNameForNode(node); return lua.createIdentifier(this.createTempName(name), node, tempSymbolId); } diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index 8197ff5da..0670c51fc 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -161,7 +161,7 @@ export function createLocalOrExportedOrGlobalDeclaration( // Split declaration and assignment of identifiers that reference themselves in their declaration. // Put declaration above preceding statements in case the identifier is referenced in those. const precedingDeclaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal); - context.addPrecedingStatements(precedingDeclaration, true); + context.prependPrecedingStatements(precedingDeclaration); if (rhs) { assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); } diff --git a/src/transformation/utils/preceding-statements.ts b/src/transformation/utils/preceding-statements.ts new file mode 100644 index 000000000..c64d0b90a --- /dev/null +++ b/src/transformation/utils/preceding-statements.ts @@ -0,0 +1,11 @@ +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; + +export function transformInPrecedingStatementScope< + TReturn extends lua.Statement | lua.Statement[] | lua.Expression | lua.Expression[] +>(context: TransformationContext, transformer: () => TReturn): [lua.Statement[], TReturn] { + context.pushPrecedingStatements(); + const statementOrStatements = transformer(); + const precedingStatements = context.popPrecedingStatements(); + return [precedingStatements, statementOrStatements]; +} diff --git a/src/transformation/utils/symbols.ts b/src/transformation/utils/symbols.ts index f6afdf8e6..f800eea86 100644 --- a/src/transformation/utils/symbols.ts +++ b/src/transformation/utils/symbols.ts @@ -5,8 +5,6 @@ import { TransformationContext } from "../context"; import { isOptimizedVarArgSpread } from "../visitors/spread"; import { markSymbolAsReferencedInCurrentScopes } from "./scope"; -export const tempSymbolId = -1 as lua.SymbolId; - const symbolIdCounters = new WeakMap(); function nextSymbolId(context: TransformationContext): lua.SymbolId { const symbolId = (symbolIdCounters.get(context) ?? 0) + 1; diff --git a/src/transformation/visitors/access.ts b/src/transformation/visitors/access.ts index 7157d6c9e..a3ec68c46 100644 --- a/src/transformation/visitors/access.ts +++ b/src/transformation/visitors/access.ts @@ -11,7 +11,7 @@ import { tryGetConstEnumValue } from "./enum"; import { transformOrderedExpressions } from "./expression-list"; import { isMultiReturnCall, returnsMultiType } from "./language-extensions/multi"; -function getElementAccessArgument( +function addOneToArrayAccessArgument( context: TransformationContext, node: ts.ElementAccessExpression, index: lua.Expression @@ -29,7 +29,7 @@ export function transformElementAccessArgument( node: ts.ElementAccessExpression ): lua.Expression { const index = context.transformExpression(node.argumentExpression); - return getElementAccessArgument(context, node, index); + return addOneToArrayAccessArgument(context, node, index); } export const transformElementAccessExpression: FunctionVisitor = (node, context) => { @@ -38,7 +38,7 @@ export const transformElementAccessExpression: FunctionVisitor = (node, context) => { diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index 5d1bf6d71..ce31eb7b6 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -12,6 +12,7 @@ import { isMultiReturnCall } from "../language-extensions/multi"; import { notAllowedOptionalAssignment } from "../../utils/diagnostics"; import { transformElementAccessArgument } from "../access"; import { moveToPrecedingTemp, transformExpressionList } from "../expression-list"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; export function transformAssignmentLeftHandSideExpression( context: TransformationContext, @@ -106,19 +107,19 @@ function transformDestructuredAssignmentExpression( context: TransformationContext, expression: ts.DestructuringAssignment ) { - const rootIdentifier = context.createTempForNode(expression.right); + const rootIdentifier = context.createTempNameForNode(expression.right); - context.pushPrecedingStatements(); - let right = context.transformExpression(expression.right); - const rootPrecedingStatements = context.popPrecedingStatements(); - context.addPrecedingStatements(rootPrecedingStatements); + let [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); + context.addPrecedingStatements(rightPrecedingStatements); if (isMultiReturnCall(context, expression.right)) { right = wrapInTable(right); } const statements = [ lua.createVariableDeclarationStatement(rootIdentifier, right), - ...transformDestructuringAssignment(context, expression, rootIdentifier, rootPrecedingStatements.length > 0), + ...transformDestructuringAssignment(context, expression, rootIdentifier, rightPrecedingStatements.length > 0), ]; return { statements, result: rootIdentifier }; @@ -151,10 +152,10 @@ export function transformAssignmentExpression( } if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) { - const tempVar = context.createTempForNode(expression.right); - context.pushPrecedingStatements(); - const right = context.transformExpression(expression.right); - const precedingStatements = context.popPrecedingStatements(); + const tempVar = context.createTempNameForNode(expression.right); + const [precedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); const left = transformAssignmentLeftHandSideExpression( context, @@ -231,28 +232,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); + let [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); + context.addPrecedingStatements(rightPrecedingStatements); if (isMultiReturnCall(context, expression.right)) { right = wrapInTable(right); } - const rootIdentifier = context.createTempForNode(expression.left); + const rootIdentifier = context.createTempNameForNode(expression.left); return [ lua.createVariableDeclarationStatement(rootIdentifier, right), ...transformDestructuringAssignment( context, expression, rootIdentifier, - rootPrecedingStatements.length > 0 + rightPrecedingStatements.length > 0 ), ]; } else { - context.pushPrecedingStatements(); - const right = context.transformExpression(expression.right); - const precedingStatements = context.popPrecedingStatements(); + const [precedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); 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 3b934c44f..5a7ab52f6 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -2,6 +2,7 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { cast, assertNever } from "../../../utils"; import { TransformationContext } from "../../context"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; import { transformBinaryOperation } from "../binary-expression"; import { transformAssignmentWithRightPrecedingStatements } from "./assignments"; @@ -71,20 +72,20 @@ export function transformCompoundAssignment( isPostfix: boolean ) { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); - context.pushPrecedingStatements(); - const right = context.transformExpression(rhs); - const rightPrecedingStatements = context.popPrecedingStatements(); + const [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(rhs) + ); 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.createTempForLuaExpression(left.table); - const index = context.createTempForLuaExpression(left.index); + const obj = context.createTempNameForLuaExpression(left.table); + const index = context.createTempNameForLuaExpression(left.index); const objAndIndexDeclaration = lua.createVariableDeclarationStatement([obj, index], [left.table, left.index]); const accessExpression = lua.createTableIndexExpression(obj, index); - const tmp = context.createTempForLuaExpression(left); + const tmp = context.createTempNameForLuaExpression(left); let tmpDeclaration: lua.VariableDeclarationStatement; let assignStatement: lua.AssignmentStatement; if (isPostfix) { @@ -110,7 +111,7 @@ export function transformCompoundAssignment( // local ____tmp = ${left}; // ${left} = ____tmp ${replacementOperator} ${right}; // return ____tmp - const tmpIdentifier = context.createTempForLuaExpression(left); + const tmpIdentifier = context.createTempNameForLuaExpression(left); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); const operatorExpression = transformBinaryOperation(context, tmpIdentifier, right, operator, expression); const assignStatements = transformAssignmentWithRightPrecedingStatements( @@ -125,7 +126,7 @@ export function transformCompoundAssignment( // local ____tmp = ${left} ${replacementOperator} ${right}; // ${left} = ____tmp; // return ____tmp - const tmpIdentifier = context.createTempForLuaExpression(left); + const tmpIdentifier = context.createTempNameForLuaExpression(left); const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, operatorExpression); @@ -187,16 +188,16 @@ 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 [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(rhs) + ); 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 = context.createTempForLuaExpression(left.table); - const index = context.createTempForLuaExpression(left.index); + const obj = context.createTempNameForLuaExpression(left.table); + const index = context.createTempNameForLuaExpression(left.index); const objAndIndexDeclaration = lua.createVariableDeclarationStatement([obj, index], [left.table, left.index]); const accessExpression = lua.createTableIndexExpression(obj, index); diff --git a/src/transformation/visitors/binary-expression/destructuring-assignments.ts b/src/transformation/visitors/binary-expression/destructuring-assignments.ts index 45dd152fe..6f68d4671 100644 --- a/src/transformation/visitors/binary-expression/destructuring-assignments.ts +++ b/src/transformation/visitors/binary-expression/destructuring-assignments.ts @@ -4,6 +4,7 @@ import * as lua from "../../../LuaAST"; import { assertNever, cast } from "../../../utils"; import { TransformationContext } from "../../context"; import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; import { isArrayType, isAssignmentPattern } from "../../utils/typescript"; import { moveToPrecedingTemp } from "../expression-list"; import { transformPropertyName } from "../literal"; @@ -39,22 +40,22 @@ export function transformDestructuringAssignment( context: TransformationContext, node: ts.DestructuringAssignment, root: lua.Expression, - rootHasPrecedingStatements: boolean + rightHasPrecedingStatements: boolean ): lua.Statement[] { - return transformAssignmentPattern(context, node.left, root, rootHasPrecedingStatements); + return transformAssignmentPattern(context, node.left, root, rightHasPrecedingStatements); } export function transformAssignmentPattern( context: TransformationContext, node: ts.AssignmentPattern, root: lua.Expression, - rootHasPrecedingStatements: boolean + rightHasPrecedingStatements: boolean ): lua.Statement[] { switch (node.kind) { case ts.SyntaxKind.ObjectLiteralExpression: - return transformObjectLiteralAssignmentPattern(context, node, root, rootHasPrecedingStatements); + return transformObjectLiteralAssignmentPattern(context, node, root, rightHasPrecedingStatements); case ts.SyntaxKind.ArrayLiteralExpression: - return transformArrayLiteralAssignmentPattern(context, node, root, rootHasPrecedingStatements); + return transformArrayLiteralAssignmentPattern(context, node, root, rightHasPrecedingStatements); } } @@ -62,7 +63,7 @@ function transformArrayLiteralAssignmentPattern( context: TransformationContext, node: ts.ArrayLiteralExpression, root: lua.Expression, - rootHasPrecedingStatements: boolean + rightHasPrecedingStatements: boolean ): lua.Statement[] { return node.elements.flatMap((element, index) => { const indexedRoot = lua.createTableIndexExpression(root, lua.createNumericLiteral(index + 1), element); @@ -73,17 +74,17 @@ function transformArrayLiteralAssignmentPattern( context, element as ts.ObjectLiteralExpression, indexedRoot, - rootHasPrecedingStatements + rightHasPrecedingStatements ); case ts.SyntaxKind.ArrayLiteralExpression: return transformArrayLiteralAssignmentPattern( context, element as ts.ArrayLiteralExpression, indexedRoot, - rootHasPrecedingStatements + rightHasPrecedingStatements ); case ts.SyntaxKind.BinaryExpression: - const assignedVariable = context.createTempForLuaExpression(indexedRoot); + const assignedVariable = context.createTempNameForLuaExpression(indexedRoot); const assignedVariableDeclaration = lua.createVariableDeclarationStatement( assignedVariable, @@ -96,16 +97,18 @@ function transformArrayLiteralAssignmentPattern( lua.SyntaxKind.EqualityOperator ); - context.pushPrecedingStatements(); - - const defaultAssignmentStatements = transformAssignment( + const [defaultPrecedingStatements, defaultAssignmentStatements] = transformInPrecedingStatementScope( context, - (element as ts.BinaryExpression).left, - context.transformExpression((element as ts.BinaryExpression).right) + () => + transformAssignment( + context, + (element as ts.BinaryExpression).left, + context.transformExpression((element as ts.BinaryExpression).right) + ) ); // Keep preceding statements inside if block - defaultAssignmentStatements.unshift(...context.popPrecedingStatements()); + defaultAssignmentStatements.unshift(...defaultPrecedingStatements); const elseAssignmentStatements = transformAssignment( context, @@ -123,9 +126,10 @@ function transformArrayLiteralAssignmentPattern( case ts.SyntaxKind.Identifier: case ts.SyntaxKind.PropertyAccessExpression: case ts.SyntaxKind.ElementAccessExpression: - context.pushPrecedingStatements(); - const statements = transformAssignment(context, element, indexedRoot, rootHasPrecedingStatements); - return [...context.popPrecedingStatements(), ...statements]; // Keep preceding statements in order + const [precedingStatements, statements] = transformInPrecedingStatementScope(context, () => + transformAssignment(context, element, indexedRoot, rightHasPrecedingStatements) + ); + return [...precedingStatements, ...statements]; // Keep preceding statements in order case ts.SyntaxKind.SpreadElement: if (index !== node.elements.length - 1) { // TypeScript error @@ -140,14 +144,15 @@ function transformArrayLiteralAssignmentPattern( lua.createNumericLiteral(index) ); - context.pushPrecedingStatements(); - const spreadStatements = transformAssignment( - context, - (element as ts.SpreadElement).expression, - restElements, - rootHasPrecedingStatements + const [spreadPrecedingStatements, spreadStatements] = transformInPrecedingStatementScope(context, () => + transformAssignment( + context, + (element as ts.SpreadElement).expression, + restElements, + rightHasPrecedingStatements + ) ); - return [...context.popPrecedingStatements(), ...spreadStatements]; // Keep preceding statements in order + return [...spreadPrecedingStatements, ...spreadStatements]; // Keep preceding statements in order case ts.SyntaxKind.OmittedExpression: return []; default: @@ -161,7 +166,7 @@ function transformObjectLiteralAssignmentPattern( context: TransformationContext, node: ts.ObjectLiteralExpression, root: lua.Expression, - rootHasPrecedingStatements: boolean + rightHasPrecedingStatements: boolean ): lua.Statement[] { const result: lua.Statement[] = []; @@ -171,7 +176,7 @@ function transformObjectLiteralAssignmentPattern( result.push(...transformShorthandPropertyAssignment(context, property, root)); break; case ts.SyntaxKind.PropertyAssignment: - result.push(...transformPropertyAssignment(context, property, root, rootHasPrecedingStatements)); + result.push(...transformPropertyAssignment(context, property, root, rightHasPrecedingStatements)); break; case ts.SyntaxKind.SpreadAssignment: result.push(...transformSpreadAssignment(context, property, root, node.properties)); @@ -230,7 +235,7 @@ function transformPropertyAssignment( context: TransformationContext, node: ts.PropertyAssignment, root: lua.Expression, - rootHasPrecedingStatements: boolean + rightHasPrecedingStatements: boolean ): lua.Statement[] { const result: lua.Statement[] = []; @@ -243,7 +248,7 @@ function transformPropertyAssignment( context, node.initializer, newRootAccess, - rootHasPrecedingStatements + rightHasPrecedingStatements ); } @@ -252,7 +257,7 @@ function transformPropertyAssignment( context, node.initializer, newRootAccess, - rootHasPrecedingStatements + rightHasPrecedingStatements ); } } @@ -273,12 +278,13 @@ function transformPropertyAssignment( // Access expressions need their table and index expressions cached to preserve execution order const left = cast(context.transformExpression(node.initializer.left), lua.isTableIndexExpression); - context.pushPrecedingStatements(); - const defaultExpression = context.transformExpression(node.initializer.right); - const defaultPrecedingStatements = context.popPrecedingStatements(); + const rightExpression = node.initializer.right; + const [defaultPrecedingStatements, defaultExpression] = transformInPrecedingStatementScope(context, () => + context.transformExpression(rightExpression) + ); - const tableTemp = context.createTempForLuaExpression(left.table); - const indexTemp = context.createTempForLuaExpression(left.index); + const tableTemp = context.createTempNameForLuaExpression(left.table); + const indexTemp = context.createTempNameForLuaExpression(left.index); const tempsDeclaration = lua.createVariableDeclarationStatement( [tableTemp, indexTemp], @@ -318,7 +324,7 @@ function transformPropertyAssignment( context, node.initializer, extractingExpression, - rootHasPrecedingStatements + rightHasPrecedingStatements ); } diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 4ef7719bc..90ee079fb 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 { transformOrderedExpressions } from "../expression-list"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; type SimpleOperator = | ts.AdditiveOperatorOrHigher @@ -83,11 +84,11 @@ function createShortCircuitBinaryExpression( createCondition: (identifier: lua.Identifier) => lua.Expression ) { const lhs = context.transformExpression(node.left); - context.pushPrecedingStatements(); - const rhs = context.transformExpression(node.right); - const rightPrecedingStatements = context.popPrecedingStatements(); + const [rightPrecedingStatements, rhs] = transformInPrecedingStatementScope(context, () => + context.transformExpression(node.right) + ); if (rightPrecedingStatements.length > 0) { - const result = context.createTempForLuaExpression(lhs); + const result = context.createTempNameForLuaExpression(lhs); const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs, node.left); const ifStatement = lua.createIfStatement( createCondition(lua.cloneIdentifier(result)), @@ -145,9 +146,10 @@ export const transformBinaryExpression: FunctionVisitor = ( case ts.SyntaxKind.CommaToken: { const statements = context.transformStatements(ts.factory.createExpressionStatement(node.left)); - context.pushPrecedingStatements(); - const result = context.transformExpression(node.right); - statements.push(...context.popPrecedingStatements()); + const [precedingStatements, result] = transformInPrecedingStatementScope(context, () => + context.transformExpression(node.right) + ); + statements.push(...precedingStatements); context.addPrecedingStatements(statements); return result; } diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 36bf8b1ca..a863789ae 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -24,6 +24,7 @@ import { } from "./language-extensions/table"; import { annotationRemoved, invalidTableDeleteExpression, invalidTableSetExpression } from "../utils/diagnostics"; import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; export type PropertyCallExpression = ts.CallExpression & { expression: ts.PropertyAccessExpression }; @@ -91,9 +92,9 @@ export function transformCallAndArguments( signature?: ts.Signature, callContext?: ts.Expression ): [lua.Expression, lua.Expression[]] { - context.pushPrecedingStatements(); - const transformedArguments = transformArguments(context, params, signature, callContext); - const argPrecedingStatements = context.popPrecedingStatements(); + const [argPrecedingStatements, transformedArguments] = transformInPrecedingStatementScope(context, () => + transformArguments(context, params, signature, callContext) + ); return transformCallWithArguments(context, callExpression, transformedArguments, argPrecedingStatements); } @@ -133,9 +134,9 @@ export function transformContextualCallExpression( ): lua.CallExpression | lua.MethodCallExpression { const left = ts.isCallExpression(node) ? node.expression : node.tag; - context.pushPrecedingStatements(); - let transformedArguments = transformArguments(context, args, signature); - const argPrecedingStatements = context.popPrecedingStatements(); + let [argPrecedingStatements, transformedArguments] = transformInPrecedingStatementScope(context, () => + transformArguments(context, args, signature) + ); if ( ts.isPropertyAccessExpression(left) && diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts index 1a6cf2ee3..f83837a2d 100644 --- a/src/transformation/visitors/conditional.ts +++ b/src/transformation/visitors/conditional.ts @@ -1,6 +1,7 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { FunctionVisitor, TransformationContext } from "../context"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope"; import { transformBlockOrStatement } from "./block"; @@ -31,18 +32,18 @@ function transformProtectedConditionalExpression( context: TransformationContext, expression: ts.ConditionalExpression ): lua.Expression { - const tempVar = context.createTempForNode(expression.condition); + const tempVar = context.createTempNameForNode(expression.condition); const condition = context.transformExpression(expression.condition); - context.pushPrecedingStatements(); - const val1 = context.transformExpression(expression.whenTrue); - const trueStatements = context.popPrecedingStatements(); + const [trueStatements, val1] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.whenTrue) + ); trueStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val1, expression.whenTrue)); - context.pushPrecedingStatements(); - const val2 = context.transformExpression(expression.whenFalse); - const falseStatements = context.popPrecedingStatements(); + const [falseStatements, val2] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.whenFalse) + ); falseStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val2, expression.whenFalse)); context.addPrecedingStatements([ @@ -80,9 +81,18 @@ export function transformIfStatement(statement: ts.IfStatement, context: Transfo if (statement.elseStatement) { if (ts.isIfStatement(statement.elseStatement)) { - context.pushPrecedingStatements(); - const elseStatement = transformIfStatement(statement.elseStatement, context); - const precedingStatements = context.popPrecedingStatements(); + const tsElseStatement = statement.elseStatement; + const [precedingStatements, elseStatement] = transformInPrecedingStatementScope(context, () => + transformIfStatement(tsElseStatement, context) + ); + // If else-if condition generates preceding statements, we can't use elseif, we have to break it down: + // if conditionA then + // ... + // else + // conditionB's preceding statements + // if conditionB then + // end + // end if (precedingStatements.length > 0) { const elseBlock = lua.createBlock([...precedingStatements, elseStatement]); return lua.createIfStatement(condition, ifBlock, elseBlock); diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 4722c4a8d..5cd5a0e5b 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -1,9 +1,8 @@ import assert = require("assert"); import * as ts from "typescript"; import * as lua from "../../LuaAST"; -import { TransformationContext } from "../context"; +import { TransformationContext, tempSymbolId } from "../context"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; -import { tempSymbolId } from "../utils/symbols"; import { isConstIdentifier } from "../utils/typescript"; function shouldMoveToTemp(context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node) { @@ -23,7 +22,7 @@ export function moveToPrecedingTemp( if (!shouldMoveToTemp(context, expression, tsOriginal)) { return expression; } - const tempIdentifier = context.createTempForLuaExpression(expression); + const tempIdentifier = context.createTempNameForLuaExpression(expression); const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression); lua.setNodePosition(tempDeclaration, lua.getOriginalPos(expression)); context.addPrecedingStatements(tempDeclaration); diff --git a/src/transformation/visitors/function.ts b/src/transformation/visitors/function.ts index 3c250f74e..dd749d202 100644 --- a/src/transformation/visitors/function.ts +++ b/src/transformation/visitors/function.ts @@ -13,6 +13,7 @@ import { wrapInTable, } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { peekScope, performHoisting, popScope, pushScope, Scope, ScopeType } from "../utils/scope"; import { isAsyncFunction, wrapInAsyncAwaiter } from "./async-await"; import { transformIdentifier } from "./identifier"; @@ -52,9 +53,9 @@ 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); - const precedingStatements = context.popPrecedingStatements(); + const [precedingStatements, returnStatement] = transformInPrecedingStatementScope(context, () => + transformExpressionBodyToReturnStatement(context, body) + ); return [...precedingStatements, returnStatement]; } diff --git a/src/transformation/visitors/loops/do-while.ts b/src/transformation/visitors/loops/do-while.ts index 73148605e..adf0429c9 100644 --- a/src/transformation/visitors/loops/do-while.ts +++ b/src/transformation/visitors/loops/do-while.ts @@ -1,16 +1,18 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { FunctionVisitor } from "../../context"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; import { invertCondition, transformLoopBody } from "./utils"; export const transformWhileStatement: FunctionVisitor = (statement, context) => { const body = transformLoopBody(context, statement); - context.pushPrecedingStatements(); - let condition = context.transformExpression(statement.expression); - const precedingStatements = context.popPrecedingStatements(); + let [precedingStatements, condition] = transformInPrecedingStatementScope(context, () => + context.transformExpression(statement.expression) + ); - // Change from 'while condition' to 'while true - if not condition then break' + // Change from 'while condition' to 'while true - [preceding statements] if not condition then break' + // This is so that preceding statements are evaluated every iteration. if (precedingStatements.length > 0) { precedingStatements.push( lua.createIfStatement( @@ -30,11 +32,11 @@ export const transformWhileStatement: FunctionVisitor = (stat export const transformDoStatement: FunctionVisitor = (statement, context) => { const body = lua.createDoStatement(transformLoopBody(context, statement)); - context.pushPrecedingStatements(); - let condition = invertCondition(context.transformExpression(statement.expression)); - const precedingStatements = context.popPrecedingStatements(); + let [precedingStatements, condition] = transformInPrecedingStatementScope(context, () => + invertCondition(context.transformExpression(statement.expression)) + ); - // Change from 'repeat until not condition' to 'repeat - if not condition break - until false' + // Change from 'repeat until not condition' to 'repeat - [preceding statements] if not condition break - until false' if (precedingStatements.length > 0) { precedingStatements.push( lua.createIfStatement( diff --git a/src/transformation/visitors/loops/for.ts b/src/transformation/visitors/loops/for.ts index cf1665936..f47ae4ec9 100644 --- a/src/transformation/visitors/loops/for.ts +++ b/src/transformation/visitors/loops/for.ts @@ -1,6 +1,7 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { FunctionVisitor } from "../../context"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; import { checkVariableDeclarationList, transformVariableDeclaration } from "../variable-declaration"; import { invertCondition, transformLoopBody } from "./utils"; @@ -21,11 +22,13 @@ export const transformForStatement: FunctionVisitor = (statemen let condition: lua.Expression; if (statement.condition) { - context.pushPrecedingStatements(); - condition = context.transformExpression(statement.condition); - const precedingStatements = context.popPrecedingStatements(); + let precedingStatements: lua.Statement[]; + const tsCondition = statement.condition; + [precedingStatements, condition] = transformInPrecedingStatementScope(context, () => + context.transformExpression(tsCondition) + ); - // Change 'while condition' to 'while true - if not condition break' + // Change 'while condition' to 'while true - [preceding statements] if not condition break' if (precedingStatements.length > 0) { precedingStatements.push( lua.createIfStatement( diff --git a/src/transformation/visitors/sourceFile.ts b/src/transformation/visitors/sourceFile.ts index bd4c2c539..903ec0a11 100644 --- a/src/transformation/visitors/sourceFile.ts +++ b/src/transformation/visitors/sourceFile.ts @@ -4,6 +4,7 @@ import { assert } from "../../utils"; import { FunctionVisitor } from "../context"; import { createExportsIdentifier } from "../utils/lua-ast"; import { getUsedLuaLibFeatures } from "../utils/lualib"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope"; import { hasExportEquals } from "../utils/typescript"; @@ -13,9 +14,10 @@ export const transformSourceFileNode: FunctionVisitor = (node, co const [statement] = node.statements; if (statement) { assert(ts.isExpressionStatement(statement)); - context.pushPrecedingStatements(); - const expression = context.transformExpression(statement.expression); - statements.push(...context.popPrecedingStatements()); + const [precedingStatements, expression] = transformInPrecedingStatementScope(context, () => + context.transformExpression(statement.expression) + ); + statements.push(...precedingStatements); statements.push(lua.createReturnStatement([expression])); } else { const errorCall = lua.createCallExpression(lua.createIdentifier("error"), [ diff --git a/src/transformation/visitors/variable-declaration.ts b/src/transformation/visitors/variable-declaration.ts index 4802c00d5..2c5588dd8 100644 --- a/src/transformation/visitors/variable-declaration.ts +++ b/src/transformation/visitors/variable-declaration.ts @@ -7,6 +7,7 @@ import { unsupportedVarDeclaration } from "../utils/diagnostics"; import { addExportToIdentifier } from "../utils/export"; import { createLocalOrExportedOrGlobalDeclaration, createUnpackCall, wrapInTable } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { transformIdentifier } from "./identifier"; import { isMultiReturnCall } from "./language-extensions/multi"; import { transformPropertyName } from "./literal"; @@ -63,9 +64,11 @@ export function transformBindingPattern( // The identifier of the new variable const variableName = transformIdentifier(context, element.name); // The field to extract - context.pushPrecedingStatements(); - const propertyName = transformPropertyName(context, element.propertyName ?? element.name); - result.push(...context.popPrecedingStatements()); // Keep property's preceding statements in order + const elementName = element.propertyName ?? element.name; + const [precedingStatements, propertyName] = transformInPrecedingStatementScope(context, () => + transformPropertyName(context, elementName) + ); + result.push(...precedingStatements); // Keep property's preceding statements in order let expression: lua.Expression; if (element.dotDotDotToken) { @@ -125,9 +128,10 @@ export function transformBindingPattern( result.push(...createLocalOrExportedOrGlobalDeclaration(context, variableName, expression)); if (element.initializer) { const identifier = addExportToIdentifier(context, variableName); - context.pushPrecedingStatements(); - const initializer = context.transformExpression(element.initializer); - const initializerPrecedingStatements = context.popPrecedingStatements(); + const tsInitializer = element.initializer; + const [initializerPrecedingStatements, initializer] = transformInPrecedingStatementScope(context, () => + context.transformExpression(tsInitializer) + ); result.push( lua.createIfStatement( lua.createBinaryExpression(identifier, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator), @@ -162,7 +166,7 @@ export function transformBindingVariableDeclaration( } else { // Contain the expression in a temporary variable if (initializer) { - table = context.createTempForNode(initializer); + table = context.createTempNameForNode(initializer); let expression = context.transformExpression(initializer); if (isMultiReturnCall(context, initializer)) { expression = wrapInTable(expression); From 93923d4dad87f0d6ca975518b0bff0523eb56d47 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 7 Nov 2021 09:28:02 -0700 Subject: [PATCH 40/48] change void expressions to use preceding statements --- src/transformation/visitors/void.ts | 21 ++++++++------------- test/unit/precedingStatements.spec.ts | 11 +++++++++++ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/transformation/visitors/void.ts b/src/transformation/visitors/void.ts index 569198a93..9d6af960b 100644 --- a/src/transformation/visitors/void.ts +++ b/src/transformation/visitors/void.ts @@ -2,26 +2,21 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; import { FunctionVisitor } from "../context/visitors"; -import { createImmediatelyInvokedFunctionExpression } from "../utils/lua-ast"; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void export const transformVoidExpression: FunctionVisitor = (node, context) => { // If content is a literal it is safe to replace the entire expression with nil - if (ts.isLiteralExpression(node.expression)) { - return lua.createNilLiteral(node); - } - - // (function() local ____ = end)() - return createImmediatelyInvokedFunctionExpression( - [ + if (!ts.isLiteralExpression(node.expression)) { + // local ____ = + context.addPrecedingStatements( lua.createVariableDeclarationStatement( lua.createAnonymousIdentifier(), context.transformExpression(node.expression) - ), - ], - [], - node - ); + ) + ); + } + + return lua.createNilLiteral(node); }; export const transformVoidExpressionStatement = (node: ts.VoidExpression, context: TransformationContext) => diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 2e79bfc7e..cba6810ef 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -150,6 +150,17 @@ describe("execution order", () => { return [result, i]; `.expectToMatchJsResult(); }); + + test("void expression", () => { + util.testFunction` + function foo(x: number, y: number, z: number | undefined) { + return z; + } + let i = 0; + const result = foo(i, i++, void(i++)); + return {result, i}; + `.expectToMatchJsResult(); + }); }); describe("assignment execution order", () => { From d25377a4bc4610ce3ca9961304b24ba80b847c35 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 7 Nov 2021 14:15:49 -0700 Subject: [PATCH 41/48] better comments and variable names for preceding statement handling of loop conditions --- src/transformation/visitors/loops/do-while.ts | 37 +++++++++++++------ src/transformation/visitors/loops/for.ts | 20 +++++++--- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/transformation/visitors/loops/do-while.ts b/src/transformation/visitors/loops/do-while.ts index adf0429c9..77f172007 100644 --- a/src/transformation/visitors/loops/do-while.ts +++ b/src/transformation/visitors/loops/do-while.ts @@ -7,14 +7,21 @@ import { invertCondition, transformLoopBody } from "./utils"; export const transformWhileStatement: FunctionVisitor = (statement, context) => { const body = transformLoopBody(context, statement); - let [precedingStatements, condition] = transformInPrecedingStatementScope(context, () => + let [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () => context.transformExpression(statement.expression) ); - // Change from 'while condition' to 'while true - [preceding statements] if not condition then break' - // This is so that preceding statements are evaluated every iteration. - if (precedingStatements.length > 0) { - precedingStatements.push( + // If condition has preceding statements, ensure they are executed every iteration by using the form: + // + // while true do + // condition's preceding statements + // if not condition then + // break + // end + // ... + // end + if (conditionPrecedingStatements.length > 0) { + conditionPrecedingStatements.push( lua.createIfStatement( invertCondition(condition), lua.createBlock([lua.createBreakStatement()]), @@ -22,7 +29,7 @@ export const transformWhileStatement: FunctionVisitor = (stat statement.expression ) ); - body.unshift(...precedingStatements); + body.unshift(...conditionPrecedingStatements); condition = lua.createBooleanLiteral(true); } @@ -32,13 +39,21 @@ export const transformWhileStatement: FunctionVisitor = (stat export const transformDoStatement: FunctionVisitor = (statement, context) => { const body = lua.createDoStatement(transformLoopBody(context, statement)); - let [precedingStatements, condition] = transformInPrecedingStatementScope(context, () => + let [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () => invertCondition(context.transformExpression(statement.expression)) ); - // Change from 'repeat until not condition' to 'repeat - [preceding statements] if not condition break - until false' - if (precedingStatements.length > 0) { - precedingStatements.push( + // If condition has preceding statements, ensure they are executed every iteration by using the form: + // + // repeat + // ... + // condition's preceding statements + // if condition then + // break + // end + // end + if (conditionPrecedingStatements.length > 0) { + conditionPrecedingStatements.push( lua.createIfStatement( condition, lua.createBlock([lua.createBreakStatement()]), @@ -49,5 +64,5 @@ export const transformDoStatement: FunctionVisitor = (statement, condition = lua.createBooleanLiteral(false); } - return lua.createRepeatStatement(lua.createBlock([body, ...precedingStatements]), condition, statement); + return lua.createRepeatStatement(lua.createBlock([body, ...conditionPrecedingStatements]), condition, statement); }; diff --git a/src/transformation/visitors/loops/for.ts b/src/transformation/visitors/loops/for.ts index f47ae4ec9..3fbbb6353 100644 --- a/src/transformation/visitors/loops/for.ts +++ b/src/transformation/visitors/loops/for.ts @@ -22,15 +22,23 @@ export const transformForStatement: FunctionVisitor = (statemen let condition: lua.Expression; if (statement.condition) { - let precedingStatements: lua.Statement[]; + let conditionPrecedingStatements: lua.Statement[]; const tsCondition = statement.condition; - [precedingStatements, condition] = transformInPrecedingStatementScope(context, () => + [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () => context.transformExpression(tsCondition) ); - // Change 'while condition' to 'while true - [preceding statements] if not condition break' - if (precedingStatements.length > 0) { - precedingStatements.push( + // If condition has preceding statements, ensure they are executed every iteration by using the form: + // + // while true do + // condition's preceding statements + // if not condition then + // break + // end + // ... + // end + if (conditionPrecedingStatements.length > 0) { + conditionPrecedingStatements.push( lua.createIfStatement( invertCondition(condition), lua.createBlock([lua.createBreakStatement()]), @@ -38,7 +46,7 @@ export const transformForStatement: FunctionVisitor = (statemen statement.condition ) ); - body.unshift(...precedingStatements); + body.unshift(...conditionPrecedingStatements); condition = lua.createBooleanLiteral(true); } } else { From 69649272040cb5544b907909041185b58912cbc0 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 14 Nov 2021 06:16:50 -0700 Subject: [PATCH 42/48] addressed feedback --- .../visitors/expression-list.ts | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts index 5cd5a0e5b..01213ce4d 100644 --- a/src/transformation/visitors/expression-list.ts +++ b/src/transformation/visitors/expression-list.ts @@ -3,6 +3,7 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { TransformationContext, tempSymbolId } from "../context"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { isConstIdentifier } from "../utils/typescript"; function shouldMoveToTemp(context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node) { @@ -23,31 +24,33 @@ export function moveToPrecedingTemp( return expression; } const tempIdentifier = context.createTempNameForLuaExpression(expression); - const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression); - lua.setNodePosition(tempDeclaration, lua.getOriginalPos(expression)); + const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression, tsOriginal); context.addPrecedingStatements(tempDeclaration); - const tempClone = lua.cloneIdentifier(tempIdentifier); - lua.setNodePosition(tempClone, lua.getOriginalPos(tempIdentifier)); - return tempClone; + return lua.cloneIdentifier(tempIdentifier, tsOriginal); } function transformExpressions( context: TransformationContext, expressions: readonly ts.Expression[] -): [lua.Expression[], lua.Statement[][], number] { +): { + transformedExpressions: lua.Expression[]; + precedingStatements: lua.Statement[][]; + lastPrecedingStatementsIndex: number; +} { const precedingStatements: lua.Statement[][] = []; - const transformExpressions: lua.Expression[] = []; + const transformedExpressions: lua.Expression[] = []; let lastPrecedingStatementsIndex = -1; for (let i = 0; i < expressions.length; ++i) { - context.pushPrecedingStatements(); - transformExpressions.push(context.transformExpression(expressions[i])); - const expressionPrecedingStatements = context.popPrecedingStatements(); + const [expressionPrecedingStatements, expression] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expressions[i]) + ); + transformedExpressions.push(expression); if (expressionPrecedingStatements.length > 0) { lastPrecedingStatementsIndex = i; } precedingStatements.push(expressionPrecedingStatements); } - return [transformExpressions, precedingStatements, lastPrecedingStatementsIndex]; + return { transformedExpressions, precedingStatements, lastPrecedingStatementsIndex }; } function transformExpressionsUsingTemps( @@ -142,7 +145,7 @@ export function transformExpressionList( context: TransformationContext, expressions: readonly ts.Expression[] ): lua.Expression[] { - const [transformedExpressions, precedingStatements, lastPrecedingStatementsIndex] = transformExpressions( + const { transformedExpressions, precedingStatements, lastPrecedingStatementsIndex } = transformExpressions( context, expressions ); @@ -175,7 +178,7 @@ export function transformOrderedExpressions( context: TransformationContext, expressions: readonly ts.Expression[] ): lua.Expression[] { - const [transformedExpressions, precedingStatements, lastPrecedingStatementsIndex] = transformExpressions( + const { transformedExpressions, precedingStatements, lastPrecedingStatementsIndex } = transformExpressions( context, expressions ); From 1f2d638ce9ce8e3f6202730dab4f26133124d404 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 14 Nov 2021 06:24:01 -0700 Subject: [PATCH 43/48] reverted change that didn't seem necessary --- src/transformation/visitors/binary-expression/assignments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index ce31eb7b6..aec1ee96c 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -78,7 +78,7 @@ export function transformAssignment( const left = transformAssignmentLeftHandSideExpression(context, lhs, rightHasPrecedingStatements); - const rootAssignment = lua.createAssignmentStatement(left, right, lhs); + const rootAssignment = lua.createAssignmentStatement(left, right, lhs.parent); return [ rootAssignment, From 738836fc5a1856b4af79ef32dd9a95c841091747 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 15 Nov 2021 07:17:36 -0700 Subject: [PATCH 44/48] updated transformBinaryOperation to handle preceding statements --- .../visitors/binary-expression/compound.ts | 73 ++++++++++-- .../destructuring-assignments.ts | 18 +-- .../visitors/binary-expression/index.ts | 109 +++++++++++++++--- src/transformation/visitors/typeof.ts | 11 +- test/unit/precedingStatements.spec.ts | 15 ++- 5 files changed, 187 insertions(+), 39 deletions(-) diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index 5a7ab52f6..1830915bf 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -72,7 +72,7 @@ export function transformCompoundAssignment( isPostfix: boolean ) { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); - const [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + let [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => context.transformExpression(rhs) ); @@ -88,16 +88,31 @@ export function transformCompoundAssignment( const tmp = context.createTempNameForLuaExpression(left); let tmpDeclaration: lua.VariableDeclarationStatement; let assignStatement: lua.AssignmentStatement; + let operatorExpression: lua.Expression; if (isPostfix) { // local ____tmp = ____obj[____index]; // ____obj[____index] = ____tmp ${replacementOperator} ${right}; tmpDeclaration = lua.createVariableDeclarationStatement(tmp, accessExpression); - const operatorExpression = transformBinaryOperation(context, tmp, right, operator, expression); + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + tmp, + right, + rightPrecedingStatements, + operator, + expression + ); assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); } else { // local ____tmp = ____obj[____index] ${replacementOperator} ${right}; // ____obj[____index] = ____tmp; - const operatorExpression = transformBinaryOperation(context, accessExpression, right, operator, expression); + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + accessExpression, + right, + rightPrecedingStatements, + operator, + expression + ); tmpDeclaration = lua.createVariableDeclarationStatement(tmp, operatorExpression); assignStatement = lua.createAssignmentStatement(accessExpression, tmp); } @@ -113,7 +128,15 @@ export function transformCompoundAssignment( // return ____tmp const tmpIdentifier = context.createTempNameForLuaExpression(left); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); - const operatorExpression = transformBinaryOperation(context, tmpIdentifier, right, operator, expression); + let operatorExpression: lua.Expression; + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + tmpIdentifier, + right, + rightPrecedingStatements, + operator, + expression + ); const assignStatements = transformAssignmentWithRightPrecedingStatements( context, lhs, @@ -127,7 +150,15 @@ export function transformCompoundAssignment( // ${left} = ____tmp; // return ____tmp const tmpIdentifier = context.createTempNameForLuaExpression(left); - const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); + let operatorExpression: lua.Expression; + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + expression + ); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, operatorExpression); if (isSetterSkippingCompoundAssignmentOperator(operator)) { @@ -155,7 +186,15 @@ export function transformCompoundAssignment( // Simple expressions // ${left} = ${left} ${operator} ${right} - const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); + let operatorExpression: lua.Expression; + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + expression + ); const statements = transformAssignmentWithRightPrecedingStatements( context, lhs, @@ -188,7 +227,7 @@ export function transformCompoundAssignmentStatement( operator: CompoundAssignmentToken ): lua.Statement[] { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); - const [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + let [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => context.transformExpression(rhs) ); @@ -215,7 +254,15 @@ export function transformCompoundAssignmentStatement( ]; } - const operatorExpression = transformBinaryOperation(context, accessExpression, right, operator, node); + let operatorExpression: lua.Expression; + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + accessExpression, + right, + rightPrecedingStatements, + operator, + node + ); const assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); return [objAndIndexDeclaration, ...rightPrecedingStatements, assignStatement]; } else { @@ -225,7 +272,15 @@ export function transformCompoundAssignmentStatement( // Simple statements // ${left} = ${left} ${replacementOperator} ${right} - const operatorExpression = transformBinaryOperation(context, left, right, operator, node); + let operatorExpression: lua.Expression; + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + node + ); return transformAssignmentWithRightPrecedingStatements( context, lhs, diff --git a/src/transformation/visitors/binary-expression/destructuring-assignments.ts b/src/transformation/visitors/binary-expression/destructuring-assignments.ts index 6f68d4671..d3d0086c0 100644 --- a/src/transformation/visitors/binary-expression/destructuring-assignments.ts +++ b/src/transformation/visitors/binary-expression/destructuring-assignments.ts @@ -292,18 +292,20 @@ function transformPropertyAssignment( ); // obj[index] = extractingExpression ?? defaultExpression + const [rightPrecedingStatements, rhs] = transformBinaryOperation( + context, + extractingExpression, + defaultExpression, + defaultPrecedingStatements, + ts.SyntaxKind.QuestionQuestionToken, + node.initializer + ); const assignStatement = lua.createAssignmentStatement( lua.createTableIndexExpression(tableTemp, indexTemp), - transformBinaryOperation( - context, - extractingExpression, - defaultExpression, - ts.SyntaxKind.QuestionQuestionToken, - node.initializer - ) + rhs ); - destructureAssignmentStatements = [tempsDeclaration, ...defaultPrecedingStatements, assignStatement]; + destructureAssignmentStatements = [tempsDeclaration, ...rightPrecedingStatements, assignStatement]; } else { const assignmentLeftHandSide = context.transformExpression(node.initializer.left); diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 90ee079fb..125bc3117 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -17,6 +17,11 @@ import { assert } from "../../../utils"; import { transformOrderedExpressions } from "../expression-list"; import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; +type ShortCircuitOperator = + | ts.SyntaxKind.AmpersandAmpersandToken + | ts.SyntaxKind.BarBarToken + | ts.SyntaxKind.QuestionQuestionToken; + type SimpleOperator = | ts.AdditiveOperatorOrHigher | Exclude @@ -42,7 +47,7 @@ const simpleOperatorsToLua: Record = { [ts.SyntaxKind.ExclamationEqualsEqualsToken]: lua.SyntaxKind.InequalityOperator, }; -export function transformBinaryOperation( +function transformBinaryOperationWithNoPrecedingStatements( context: TransformationContext, left: lua.Expression, right: lua.Expression, @@ -80,27 +85,77 @@ export function transformBinaryOperation( function createShortCircuitBinaryExpression( context: TransformationContext, node: ts.BinaryExpression, - operator: BitOperator | SimpleOperator | ts.SyntaxKind.QuestionQuestionToken, + lhs: lua.Expression, + rhs: lua.Expression, + rightPrecedingStatements: lua.Statement[], + createCondition: (identifier: lua.Identifier) => lua.Expression +): [lua.Statement[], lua.Expression] { + const result = context.createTempNameForLuaExpression(lhs); + const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs, node.left); + const ifStatement = lua.createIfStatement( + createCondition(lua.cloneIdentifier(result)), + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(result, rhs)]), + undefined, + node.left + ); + return [[assignmentStatement, ifStatement], result]; +} + +function transformShortCircuitBinaryExpression( + context: TransformationContext, + node: ts.BinaryExpression, + operator: ShortCircuitOperator, createCondition: (identifier: lua.Identifier) => lua.Expression -) { +): [lua.Statement[], lua.Expression] { const lhs = context.transformExpression(node.left); const [rightPrecedingStatements, rhs] = transformInPrecedingStatementScope(context, () => context.transformExpression(node.right) ); if (rightPrecedingStatements.length > 0) { - const result = context.createTempNameForLuaExpression(lhs); - const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs, node.left); - const ifStatement = lua.createIfStatement( - createCondition(lua.cloneIdentifier(result)), - lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(result, rhs)]), - undefined, - node.left - ); - context.addPrecedingStatements([assignmentStatement, ifStatement]); - return result; + return createShortCircuitBinaryExpression(context, node, lhs, rhs, rightPrecedingStatements, createCondition); } else { - return transformBinaryOperation(context, lhs, rhs, operator, node); + return [ + rightPrecedingStatements, + transformBinaryOperationWithNoPrecedingStatements(context, lhs, rhs, operator, node), + ]; + } +} + +export function transformBinaryOperation( + context: TransformationContext, + left: lua.Expression, + right: lua.Expression, + rightPrecedingStatements: lua.Statement[], + operator: BitOperator | SimpleOperator | ts.SyntaxKind.QuestionQuestionToken, + node: ts.Node +): [lua.Statement[], lua.Expression] { + if (rightPrecedingStatements.length > 0) { + switch (operator) { + case ts.SyntaxKind.QuestionQuestionToken: { + assert(ts.isBinaryExpression(node)); + return createShortCircuitBinaryExpression(context, node, left, right, rightPrecedingStatements, i => + lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator, node) + ); + } + + case ts.SyntaxKind.BarBarToken: { + assert(ts.isBinaryExpression(node)); + return createShortCircuitBinaryExpression(context, node, left, right, rightPrecedingStatements, i => + lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator, node) + ); + } + + case ts.SyntaxKind.AmpersandAmpersandToken: { + assert(ts.isBinaryExpression(node)); + return createShortCircuitBinaryExpression(context, node, left, right, rightPrecedingStatements, i => i); + } + } } + + return [ + rightPrecedingStatements, + transformBinaryOperationWithNoPrecedingStatements(context, left, right, operator, node), + ]; } export const transformBinaryExpression: FunctionVisitor = (node, context) => { @@ -155,24 +210,40 @@ export const transformBinaryExpression: FunctionVisitor = ( } case ts.SyntaxKind.QuestionQuestionToken: { - return createShortCircuitBinaryExpression(context, node, operator, i => + const [precedingStatements, result] = transformShortCircuitBinaryExpression(context, node, operator, i => lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator, node) ); + context.addPrecedingStatements(precedingStatements); + return result; } case ts.SyntaxKind.BarBarToken: { - return createShortCircuitBinaryExpression(context, node, operator, i => + const [precedingStatements, result] = transformShortCircuitBinaryExpression(context, node, operator, i => lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator, node) ); + context.addPrecedingStatements(precedingStatements); + return result; } case ts.SyntaxKind.AmpersandAmpersandToken: { - return createShortCircuitBinaryExpression(context, node, operator, i => i); + const [precedingStatements, result] = transformShortCircuitBinaryExpression( + context, + node, + operator, + i => i + ); + context.addPrecedingStatements(precedingStatements); + return result; } } - const [lhs, rhs] = transformOrderedExpressions(context, [node.left, node.right]); - return transformBinaryOperation(context, lhs, rhs, operator, node); + let [precedingStatements, [lhs, rhs]] = transformInPrecedingStatementScope(context, () => + transformOrderedExpressions(context, [node.left, node.right]) + ); + let result: lua.Expression; + [precedingStatements, result] = transformBinaryOperation(context, lhs, rhs, precedingStatements, operator, node); + context.addPrecedingStatements(precedingStatements); + return result; }; export function transformBinaryExpressionStatement( diff --git a/src/transformation/visitors/typeof.ts b/src/transformation/visitors/typeof.ts index 60eef310c..aaa094491 100644 --- a/src/transformation/visitors/typeof.ts +++ b/src/transformation/visitors/typeof.ts @@ -47,5 +47,14 @@ export function transformTypeOfBinaryExpression( const innerExpression = context.transformExpression(typeOfExpression.expression); const typeCall = lua.createCallExpression(lua.createIdentifier("type"), [innerExpression], typeOfExpression); - return transformBinaryOperation(context, typeCall, comparedExpression, operator, node); + const [precedingStatements, result] = transformBinaryOperation( + context, + typeCall, + comparedExpression, + [], + operator, + node + ); + context.addPrecedingStatements(precedingStatements); + return result; } diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index cba6810ef..6ff6b4706 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -1,6 +1,6 @@ import * as util from "../util"; -test.each([ +const shortCircuitTests: Array<{ operator: string; testValue: unknown }> = [ { operator: "&&", testValue: true }, { operator: "&&", testValue: false }, { operator: "&&", testValue: null }, @@ -19,7 +19,9 @@ test.each([ { operator: "??=", testValue: true }, { operator: "??=", testValue: false }, { operator: "??=", testValue: null }, -])("short circuit operator (%p)", ({ operator, testValue }) => { +]; + +test.each(shortCircuitTests)("short circuit operator (%p)", ({ operator, testValue }) => { util.testFunction` let x: unknown = ${testValue}; let y = 1; @@ -28,6 +30,15 @@ test.each([ `.expectToMatchJsResult(); }); +test.each(shortCircuitTests)("short circuit operator on property (%p)", ({ operator, testValue }) => { + util.testFunction` + let x: { foo: unknown } = { foo: ${testValue} }; + let y = 1; + const z = x.foo ${operator} y++; + return {x: x.foo, y, z}; + `.expectToMatchJsResult(); +}); + test.each([true, false])("ternary operator (%p)", condition => { util.testFunction` let a = 0, b = 0; From 426447935f040fe4f72839ae8ebe0d25522299df Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 16 Nov 2021 09:11:03 -0700 Subject: [PATCH 45/48] reverted switch handling to using lua expressions instead of manufactured ts expressions and handled preceding statements manually --- .../visitors/binary-expression/index.ts | 100 ++++++------ src/transformation/visitors/switch.ts | 142 ++++++++++-------- 2 files changed, 128 insertions(+), 114 deletions(-) diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 125bc3117..6c75d0a96 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -22,6 +22,11 @@ type ShortCircuitOperator = | ts.SyntaxKind.BarBarToken | ts.SyntaxKind.QuestionQuestionToken; +const isShortCircuitOperator = (value: unknown): value is ShortCircuitOperator => + value === ts.SyntaxKind.AmpersandAmpersandToken || + value === ts.SyntaxKind.BarBarToken || + value === ts.SyntaxKind.QuestionQuestionToken; + type SimpleOperator = | ts.AdditiveOperatorOrHigher | Exclude @@ -82,37 +87,59 @@ function transformBinaryOperationWithNoPrecedingStatements( return lua.createBinaryExpression(left, right, luaOperator, node); } -function createShortCircuitBinaryExpression( +export function createShortCircuitBinaryExpression( context: TransformationContext, - node: ts.BinaryExpression, lhs: lua.Expression, rhs: lua.Expression, rightPrecedingStatements: lua.Statement[], - createCondition: (identifier: lua.Identifier) => lua.Expression + operator: ShortCircuitOperator, + node?: ts.BinaryExpression ): [lua.Statement[], lua.Expression] { - const result = context.createTempNameForLuaExpression(lhs); - const assignmentStatement = lua.createVariableDeclarationStatement(result, lhs, node.left); + const conditionIdentifier = context.createTempNameForLuaExpression(lhs); + const assignmentStatement = lua.createVariableDeclarationStatement(conditionIdentifier, lhs, node?.left); + + let condition: lua.Expression; + switch (operator) { + case ts.SyntaxKind.BarBarToken: + condition = lua.createUnaryExpression( + lua.cloneIdentifier(conditionIdentifier), + lua.SyntaxKind.NotOperator, + node + ); + break; + case ts.SyntaxKind.AmpersandAmpersandToken: + condition = lua.cloneIdentifier(conditionIdentifier); + break; + case ts.SyntaxKind.QuestionQuestionToken: + condition = lua.createBinaryExpression( + lua.cloneIdentifier(conditionIdentifier), + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator, + node + ); + break; + } + const ifStatement = lua.createIfStatement( - createCondition(lua.cloneIdentifier(result)), - lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(result, rhs)]), + condition, + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(conditionIdentifier, rhs)]), undefined, - node.left + node?.left ); - return [[assignmentStatement, ifStatement], result]; + return [[assignmentStatement, ifStatement], conditionIdentifier]; } function transformShortCircuitBinaryExpression( context: TransformationContext, node: ts.BinaryExpression, - operator: ShortCircuitOperator, - createCondition: (identifier: lua.Identifier) => lua.Expression + operator: ShortCircuitOperator ): [lua.Statement[], lua.Expression] { const lhs = context.transformExpression(node.left); const [rightPrecedingStatements, rhs] = transformInPrecedingStatementScope(context, () => context.transformExpression(node.right) ); if (rightPrecedingStatements.length > 0) { - return createShortCircuitBinaryExpression(context, node, lhs, rhs, rightPrecedingStatements, createCondition); + return createShortCircuitBinaryExpression(context, lhs, rhs, rightPrecedingStatements, operator, node); } else { return [ rightPrecedingStatements, @@ -129,27 +156,9 @@ export function transformBinaryOperation( operator: BitOperator | SimpleOperator | ts.SyntaxKind.QuestionQuestionToken, node: ts.Node ): [lua.Statement[], lua.Expression] { - if (rightPrecedingStatements.length > 0) { - switch (operator) { - case ts.SyntaxKind.QuestionQuestionToken: { - assert(ts.isBinaryExpression(node)); - return createShortCircuitBinaryExpression(context, node, left, right, rightPrecedingStatements, i => - lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator, node) - ); - } - - case ts.SyntaxKind.BarBarToken: { - assert(ts.isBinaryExpression(node)); - return createShortCircuitBinaryExpression(context, node, left, right, rightPrecedingStatements, i => - lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator, node) - ); - } - - case ts.SyntaxKind.AmpersandAmpersandToken: { - assert(ts.isBinaryExpression(node)); - return createShortCircuitBinaryExpression(context, node, left, right, rightPrecedingStatements, i => i); - } - } + if (rightPrecedingStatements.length > 0 && isShortCircuitOperator(operator)) { + assert(ts.isBinaryExpression(node)); + return createShortCircuitBinaryExpression(context, left, right, rightPrecedingStatements, operator, node); } return [ @@ -209,29 +218,10 @@ export const transformBinaryExpression: FunctionVisitor = ( return result; } - case ts.SyntaxKind.QuestionQuestionToken: { - const [precedingStatements, result] = transformShortCircuitBinaryExpression(context, node, operator, i => - lua.createBinaryExpression(i, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator, node) - ); - context.addPrecedingStatements(precedingStatements); - return result; - } - + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.AmpersandAmpersandToken: case ts.SyntaxKind.BarBarToken: { - const [precedingStatements, result] = transformShortCircuitBinaryExpression(context, node, operator, i => - lua.createUnaryExpression(i, lua.SyntaxKind.NotOperator, node) - ); - context.addPrecedingStatements(precedingStatements); - return result; - } - - case ts.SyntaxKind.AmpersandAmpersandToken: { - const [precedingStatements, result] = transformShortCircuitBinaryExpression( - context, - node, - operator, - i => i - ); + const [precedingStatements, result] = transformShortCircuitBinaryExpression(context, node, operator); context.addPrecedingStatements(precedingStatements); return result; } diff --git a/src/transformation/visitors/switch.ts b/src/transformation/visitors/switch.ts index 955a650ac..2a61bd50b 100644 --- a/src/transformation/visitors/switch.ts +++ b/src/transformation/visitors/switch.ts @@ -1,7 +1,9 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; import { FunctionVisitor, TransformationContext } from "../context"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { popScope, pushScope, ScopeType, separateHoistedStatements } from "../utils/scope"; +import { createShortCircuitBinaryExpression } from "./binary-expression"; const containsBreakOrReturn = (nodes: Iterable): boolean => { for (const s of nodes) { @@ -17,51 +19,48 @@ const containsBreakOrReturn = (nodes: Iterable): boolean => { return false; }; +const createOrExpression = ( + context: TransformationContext, + left: lua.Expression, + right: lua.Expression, + rightPrecedingStatements: lua.Statement[] +): [lua.Statement[], lua.Expression] => { + if (rightPrecedingStatements.length > 0) { + return createShortCircuitBinaryExpression( + context, + left, + right, + rightPrecedingStatements, + ts.SyntaxKind.BarBarToken + ); + } else { + return [rightPrecedingStatements, lua.createBinaryExpression(left, right, lua.SyntaxKind.OrOperator)]; + } +}; + const coalesceCondition = ( - condition: ts.Expression | undefined, - switchVariable: ts.Identifier, - expression: ts.Expression -): ts.Expression => { - const comparison = ts.factory.createBinaryExpression( - switchVariable, - ts.SyntaxKind.EqualsEqualsEqualsToken, - expression + condition: lua.Expression | undefined, + conditionPrecedingStatements: lua.Statement[], + switchVariable: lua.Identifier, + expression: ts.Expression, + context: TransformationContext +): [lua.Statement[], lua.Expression] => { + const [precedingStatements, transformedExpression] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression) ); // Coalesce skipped statements + const comparison = lua.createBinaryExpression( + switchVariable, + transformedExpression, + lua.SyntaxKind.EqualityOperator + ); if (condition) { - return ts.factory.createBinaryExpression(condition, ts.SyntaxKind.BarBarToken, comparison); + return createOrExpression(context, condition, comparison, precedingStatements); } // Next condition - return comparison; -}; - -const transformCondition = ( - context: TransformationContext, - condition: ts.Expression, - conditionVariable: ts.Identifier, - isInitialCondition: boolean -): lua.Statement[] => { - // Construct TS statements and transform them to ensure preceding statements are handled correctly - if (isInitialCondition) { - // local ____cond = (____switch == x) or (____switch == y) ... - const assignment = ts.factory.createVariableStatement( - undefined, - ts.factory.createVariableDeclarationList( - [ts.factory.createVariableDeclaration(conditionVariable, undefined, undefined, condition)], - ts.NodeFlags.Let - ) - ); - return context.transformStatements(assignment); - } else { - // ____cond = ____cond or (____switch == x) or (____switch == y) ... - const assignment = ts.factory.createAssignment( - conditionVariable, - ts.factory.createBinaryExpression(conditionVariable, ts.SyntaxKind.BarBarToken, condition) - ); - return context.transformStatements(ts.factory.createExpressionStatement(assignment)); - } + return [[...conditionPrecedingStatements, ...precedingStatements], comparison]; }; export const transformSwitchStatement: FunctionVisitor = (statement, context) => { @@ -70,8 +69,8 @@ export const transformSwitchStatement: FunctionVisitor = (st // Give the switch and condition accumulator a unique name to prevent nested switches from acting up. const switchName = `____switch${scope.id}`; const conditionName = `____cond${scope.id}`; - const switchVariable = ts.factory.createIdentifier(switchName); - const conditionVariable = ts.factory.createIdentifier(conditionName); + const switchVariable = lua.createIdentifier(switchName); + const conditionVariable = lua.createIdentifier(conditionName); // If the switch only has a default clause, wrap it in a single do. // Otherwise, we need to generate a set of if statements to emulate the switch. @@ -95,7 +94,8 @@ export const transformSwitchStatement: FunctionVisitor = (st // Build up the condition for each if statement let defaultTransformed = false; let isInitialCondition = true; - let condition: ts.Expression | undefined = undefined; + let condition: lua.Expression | undefined = undefined; + let conditionPrecedingStatements: lua.Statement[] = []; for (let i = 0; i < clauses.length; i++) { const clause = clauses[i]; const previousClause: ts.CaseOrDefaultClause | undefined = clauses[i - 1]; @@ -108,32 +108,50 @@ export const transformSwitchStatement: FunctionVisitor = (st // Compute the condition for the if statement if (!ts.isDefaultClause(clause)) { - condition = coalesceCondition(condition, switchVariable, clause.expression); + [conditionPrecedingStatements, condition] = coalesceCondition( + condition, + conditionPrecedingStatements, + switchVariable, + clause.expression, + context + ); // Skip empty clauses unless final clause (i.e side-effects) if (i !== clauses.length - 1 && clause.statements.length === 0) continue; // Declare or assign condition variable - statements.push(...transformCondition(context, condition, conditionVariable, isInitialCondition)); + if (isInitialCondition) { + statements.push( + ...conditionPrecedingStatements, + lua.createVariableDeclarationStatement(conditionVariable, condition) + ); + } else { + [conditionPrecedingStatements, condition] = createOrExpression( + context, + conditionVariable, + condition, + conditionPrecedingStatements + ); + statements.push( + ...conditionPrecedingStatements, + lua.createAssignmentStatement(conditionVariable, condition) + ); + } isInitialCondition = false; } else { // If the default is proceeded by empty clauses and will be emitted we may need to initialize the condition if (isInitialCondition) { - if (condition) { - statements.push( - ...transformCondition(context, condition, conditionVariable, isInitialCondition) - ); - } else { - statements.push( - lua.createVariableDeclarationStatement( - lua.createIdentifier(conditionName), - lua.createBooleanLiteral(false) - ) - ); - } + statements.push( + ...conditionPrecedingStatements, + lua.createVariableDeclarationStatement( + conditionVariable, + condition ?? lua.createBooleanLiteral(false) + ) + ); // Clear condition ot ensure it is not evaluated twice condition = undefined; + conditionPrecedingStatements = []; isInitialCondition = false; } @@ -141,8 +159,15 @@ export const transformSwitchStatement: FunctionVisitor = (st if (i === clauses.length - 1) { // Evaluate the final condition that we may be skipping if (condition) { + [conditionPrecedingStatements, condition] = createOrExpression( + context, + conditionVariable, + condition, + conditionPrecedingStatements + ); statements.push( - ...transformCondition(context, condition, conditionVariable, isInitialCondition) + ...conditionPrecedingStatements, + lua.createAssignmentStatement(conditionVariable, condition) ); } continue; @@ -167,12 +192,11 @@ export const transformSwitchStatement: FunctionVisitor = (st } // Push if statement for case - statements.push( - lua.createIfStatement(lua.createIdentifier(conditionName), lua.createBlock(clauseStatements)) - ); + statements.push(lua.createIfStatement(conditionVariable, lua.createBlock(clauseStatements))); // Clear condition for next clause condition = undefined; + conditionPrecedingStatements = []; } // If no conditions above match, we need to create the final default case code-path, @@ -222,7 +246,7 @@ export const transformSwitchStatement: FunctionVisitor = (st // Add the switch expression after hoisting const expression = context.transformExpression(statement.expression); - statements.unshift(lua.createVariableDeclarationStatement(lua.createIdentifier(switchVariable.text), expression)); + statements.unshift(lua.createVariableDeclarationStatement(switchVariable, expression)); // Wrap the statements in a repeat until true statement to facilitate dynamic break/returns return lua.createRepeatStatement(lua.createBlock(statements), lua.createBooleanLiteral(true)); From d8eacdd5cefa4bc9c69a394b5dbec55eaf751afb Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 16 Nov 2021 14:52:50 -0700 Subject: [PATCH 46/48] constified compound assignment code --- .../visitors/binary-expression/compound.ts | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts index 1830915bf..76947ede9 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -72,7 +72,7 @@ export function transformCompoundAssignment( isPostfix: boolean ) { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); - let [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + const [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => context.transformExpression(rhs) ); @@ -86,14 +86,12 @@ export function transformCompoundAssignment( const accessExpression = lua.createTableIndexExpression(obj, index); const tmp = context.createTempNameForLuaExpression(left); - let tmpDeclaration: lua.VariableDeclarationStatement; - let assignStatement: lua.AssignmentStatement; - let operatorExpression: lua.Expression; if (isPostfix) { // local ____tmp = ____obj[____index]; // ____obj[____index] = ____tmp ${replacementOperator} ${right}; - tmpDeclaration = lua.createVariableDeclarationStatement(tmp, accessExpression); - [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + // return ____tmp + const tmpDeclaration = lua.createVariableDeclarationStatement(tmp, accessExpression); + const [precedingStatements, operatorExpression] = transformBinaryOperation( context, tmp, right, @@ -101,11 +99,16 @@ export function transformCompoundAssignment( operator, expression ); - assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); + const assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); + return { + statements: [objAndIndexDeclaration, ...precedingStatements, tmpDeclaration, assignStatement], + result: tmp, + }; } else { // local ____tmp = ____obj[____index] ${replacementOperator} ${right}; // ____obj[____index] = ____tmp; - [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + // return ____tmp + const [precedingStatements, operatorExpression] = transformBinaryOperation( context, accessExpression, right, @@ -113,14 +116,13 @@ export function transformCompoundAssignment( operator, expression ); - tmpDeclaration = lua.createVariableDeclarationStatement(tmp, operatorExpression); - assignStatement = lua.createAssignmentStatement(accessExpression, tmp); + const tmpDeclaration = lua.createVariableDeclarationStatement(tmp, operatorExpression); + const assignStatement = lua.createAssignmentStatement(accessExpression, tmp); + return { + statements: [objAndIndexDeclaration, ...precedingStatements, tmpDeclaration, assignStatement], + result: tmp, + }; } - // return ____tmp - return { - statements: [objAndIndexDeclaration, ...rightPrecedingStatements, tmpDeclaration, assignStatement], - result: tmp, - }; } else if (isPostfix) { // Postfix expressions need to cache original value in temp // local ____tmp = ${left}; @@ -128,8 +130,7 @@ export function transformCompoundAssignment( // return ____tmp const tmpIdentifier = context.createTempNameForLuaExpression(left); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); - let operatorExpression: lua.Expression; - [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + const [precedingStatements, operatorExpression] = transformBinaryOperation( context, tmpIdentifier, right, @@ -143,15 +144,14 @@ export function transformCompoundAssignment( operatorExpression, rightPrecedingStatements ); - return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; + return { statements: [tmpDeclaration, ...precedingStatements, ...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 // local ____tmp = ${left} ${replacementOperator} ${right}; // ${left} = ____tmp; // return ____tmp const tmpIdentifier = context.createTempNameForLuaExpression(left); - let operatorExpression: lua.Expression; - [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + const [precedingStatements, operatorExpression] = transformBinaryOperation( context, left, right, @@ -164,7 +164,7 @@ export function transformCompoundAssignment( if (isSetterSkippingCompoundAssignmentOperator(operator)) { const statements = [ tmpDeclaration, - ...transformSetterSkippingCompoundAssignment(tmpIdentifier, operator, right, rightPrecedingStatements), + ...transformSetterSkippingCompoundAssignment(tmpIdentifier, operator, right, precedingStatements), ]; return { statements, result: tmpIdentifier }; } @@ -173,7 +173,7 @@ export function transformCompoundAssignment( context, lhs, tmpIdentifier, - rightPrecedingStatements + precedingStatements ); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else { @@ -186,8 +186,7 @@ export function transformCompoundAssignment( // Simple expressions // ${left} = ${left} ${operator} ${right} - let operatorExpression: lua.Expression; - [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + const [precedingStatements, operatorExpression] = transformBinaryOperation( context, left, right, @@ -199,7 +198,7 @@ export function transformCompoundAssignment( context, lhs, operatorExpression, - rightPrecedingStatements + precedingStatements ); return { statements, result: left }; } From 551e951e0be47667324dd1fe6a2bfc9d9740f335 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 16 Nov 2021 14:57:54 -0700 Subject: [PATCH 47/48] renamed createShortCircuitBinaryExpression --- .../visitors/binary-expression/index.ts | 20 ++++++++++++++++--- src/transformation/visitors/switch.ts | 4 ++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 6c75d0a96..df4ef6434 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -87,7 +87,7 @@ function transformBinaryOperationWithNoPrecedingStatements( return lua.createBinaryExpression(left, right, luaOperator, node); } -export function createShortCircuitBinaryExpression( +export function createShortCircuitBinaryExpressionPrecedingStatements( context: TransformationContext, lhs: lua.Expression, rhs: lua.Expression, @@ -139,7 +139,14 @@ function transformShortCircuitBinaryExpression( context.transformExpression(node.right) ); if (rightPrecedingStatements.length > 0) { - return createShortCircuitBinaryExpression(context, lhs, rhs, rightPrecedingStatements, operator, node); + return createShortCircuitBinaryExpressionPrecedingStatements( + context, + lhs, + rhs, + rightPrecedingStatements, + operator, + node + ); } else { return [ rightPrecedingStatements, @@ -158,7 +165,14 @@ export function transformBinaryOperation( ): [lua.Statement[], lua.Expression] { if (rightPrecedingStatements.length > 0 && isShortCircuitOperator(operator)) { assert(ts.isBinaryExpression(node)); - return createShortCircuitBinaryExpression(context, left, right, rightPrecedingStatements, operator, node); + return createShortCircuitBinaryExpressionPrecedingStatements( + context, + left, + right, + rightPrecedingStatements, + operator, + node + ); } return [ diff --git a/src/transformation/visitors/switch.ts b/src/transformation/visitors/switch.ts index 2a61bd50b..e14df3002 100644 --- a/src/transformation/visitors/switch.ts +++ b/src/transformation/visitors/switch.ts @@ -3,7 +3,7 @@ import * as lua from "../../LuaAST"; import { FunctionVisitor, TransformationContext } from "../context"; import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { popScope, pushScope, ScopeType, separateHoistedStatements } from "../utils/scope"; -import { createShortCircuitBinaryExpression } from "./binary-expression"; +import { createShortCircuitBinaryExpressionPrecedingStatements } from "./binary-expression"; const containsBreakOrReturn = (nodes: Iterable): boolean => { for (const s of nodes) { @@ -26,7 +26,7 @@ const createOrExpression = ( rightPrecedingStatements: lua.Statement[] ): [lua.Statement[], lua.Expression] => { if (rightPrecedingStatements.length > 0) { - return createShortCircuitBinaryExpression( + return createShortCircuitBinaryExpressionPrecedingStatements( context, left, right, From ffd2666d050e83b3d5643c62d75421609ecfb91b Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 16 Nov 2021 16:19:03 -0700 Subject: [PATCH 48/48] Merge from master --- src/LuaLib.ts | 2 +- src/LuaPrinter.ts | 88 ++++++++++++++++--- src/lualib/Delete.ts | 10 +-- .../__snapshots__/transformation.spec.ts.snap | 26 +++++- .../translation/transformation/printFormat.ts | 27 ++++++ .../__snapshots__/expressions.spec.ts.snap | 4 +- test/unit/__snapshots__/spread.spec.ts.snap | 12 +-- test/unit/__snapshots__/switch.spec.ts.snap | 14 +-- .../__snapshots__/console.spec.ts.snap | 56 +++--------- test/unit/builtins/object.spec.ts | 39 ++++++-- .../__snapshots__/multi.spec.ts.snap | 34 ++----- .../__snapshots__/range.spec.ts.snap | 18 +--- 12 files changed, 198 insertions(+), 132 deletions(-) create mode 100644 test/translation/transformation/printFormat.ts diff --git a/src/LuaLib.ts b/src/LuaLib.ts index a03f2b447..f22ff28e1 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -109,7 +109,7 @@ const luaLibDependencies: Partial> = { Await: [LuaLibFeature.InstanceOf, LuaLibFeature.New], Decorate: [LuaLibFeature.ObjectGetOwnPropertyDescriptor, LuaLibFeature.SetDescriptor, LuaLibFeature.ObjectAssign], DelegatedYield: [LuaLibFeature.StringAccess], - Delete: [LuaLibFeature.ObjectGetOwnPropertyDescriptors], + Delete: [LuaLibFeature.ObjectGetOwnPropertyDescriptors, LuaLibFeature.Error, LuaLibFeature.New], Error: [LuaLibFeature.Class, LuaLibFeature.ClassExtends, LuaLibFeature.New], FunctionBind: [LuaLibFeature.Unpack], Generator: [LuaLibFeature.Symbol], diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index 4bfe53f5c..eb49932c8 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -120,6 +120,42 @@ export class LuaPrinter { [lua.SyntaxKind.BitwiseLeftShiftOperator]: "<<", [lua.SyntaxKind.BitwiseNotOperator]: "~", }; + private static operatorPrecedence: Record = { + [lua.SyntaxKind.OrOperator]: 1, + [lua.SyntaxKind.AndOperator]: 2, + + [lua.SyntaxKind.EqualityOperator]: 3, + [lua.SyntaxKind.InequalityOperator]: 3, + [lua.SyntaxKind.LessThanOperator]: 3, + [lua.SyntaxKind.LessEqualOperator]: 3, + [lua.SyntaxKind.GreaterThanOperator]: 3, + [lua.SyntaxKind.GreaterEqualOperator]: 3, + + [lua.SyntaxKind.BitwiseOrOperator]: 4, + [lua.SyntaxKind.BitwiseExclusiveOrOperator]: 5, + [lua.SyntaxKind.BitwiseAndOperator]: 6, + + [lua.SyntaxKind.BitwiseLeftShiftOperator]: 7, + [lua.SyntaxKind.BitwiseRightShiftOperator]: 7, + + [lua.SyntaxKind.ConcatOperator]: 8, + + [lua.SyntaxKind.AdditionOperator]: 9, + [lua.SyntaxKind.SubtractionOperator]: 9, + + [lua.SyntaxKind.MultiplicationOperator]: 10, + [lua.SyntaxKind.DivisionOperator]: 10, + [lua.SyntaxKind.FloorDivisionOperator]: 10, + [lua.SyntaxKind.ModuloOperator]: 10, + + [lua.SyntaxKind.NotOperator]: 11, + [lua.SyntaxKind.LengthOperator]: 11, + [lua.SyntaxKind.NegationOperator]: 11, + [lua.SyntaxKind.BitwiseNotOperator]: 11, + + [lua.SyntaxKind.PowerOperator]: 12, + }; + private static rightAssociativeOperators = new Set([lua.SyntaxKind.ConcatOperator, lua.SyntaxKind.PowerOperator]); private currentIndent = ""; private luaFile: string; @@ -676,34 +712,49 @@ export class LuaPrinter { const chunks: SourceChunk[] = []; chunks.push(this.printOperator(expression.operator)); - chunks.push(this.printExpressionInParenthesesIfNeeded(expression.operand)); + chunks.push( + this.printExpressionInParenthesesIfNeeded( + expression.operand, + LuaPrinter.operatorPrecedence[expression.operator] + ) + ); return this.createSourceNode(expression, chunks); } public printBinaryExpression(expression: lua.BinaryExpression): SourceNode { const chunks: SourceChunk[] = []; - - chunks.push(this.printExpressionInParenthesesIfNeeded(expression.left)); + const isRightAssociative = LuaPrinter.rightAssociativeOperators.has(expression.operator); + const precedence = LuaPrinter.operatorPrecedence[expression.operator]; + chunks.push( + this.printExpressionInParenthesesIfNeeded(expression.left, isRightAssociative ? precedence + 1 : precedence) + ); chunks.push(" ", this.printOperator(expression.operator), " "); - chunks.push(this.printExpressionInParenthesesIfNeeded(expression.right)); + chunks.push( + this.printExpressionInParenthesesIfNeeded( + expression.right, + isRightAssociative ? precedence : precedence + 1 + ) + ); return this.createSourceNode(expression, chunks); } - private printExpressionInParenthesesIfNeeded(expression: lua.Expression): SourceNode { - return this.needsParenthesis(expression) + private printExpressionInParenthesesIfNeeded(expression: lua.Expression, minPrecedenceToOmit?: number): SourceNode { + return this.needsParenthesis(expression, minPrecedenceToOmit) ? this.createSourceNode(expression, ["(", this.printExpression(expression), ")"]) : this.printExpression(expression); } - private needsParenthesis(expression: lua.Expression): boolean { - return ( - lua.isBinaryExpression(expression) || - lua.isFunctionExpression(expression) || - lua.isTableExpression(expression) || - (lua.isUnaryExpression(expression) && expression.operator === lua.SyntaxKind.NotOperator) - ); + private needsParenthesis(expression: lua.Expression, minPrecedenceToOmit?: number): boolean { + if (lua.isBinaryExpression(expression) || lua.isUnaryExpression(expression)) { + return ( + minPrecedenceToOmit === undefined || + LuaPrinter.operatorPrecedence[expression.operator] < minPrecedenceToOmit + ); + } else { + return lua.isFunctionExpression(expression) || lua.isTableExpression(expression); + } } public printCallExpression(expression: lua.CallExpression): SourceNode { @@ -769,10 +820,19 @@ export class LuaPrinter { return intersperse(chunks, ", "); } + /** + * Returns true if the expression list (table field or parameters) should be printed on one line. + */ + protected isSimpleExpressionList(expressions: lua.Expression[]): boolean { + if (expressions.length <= 1) return true; + if (expressions.length > 4) return false; + return expressions.every(isSimpleExpression); + } + protected printExpressionList(expressions: lua.Expression[]): SourceChunk[] { const chunks: SourceChunk[] = []; - if (expressions.every(isSimpleExpression)) { + if (this.isSimpleExpressionList(expressions)) { chunks.push(...this.joinChunksWithComma(expressions.map(e => this.printExpression(e)))); } else { chunks.push("\n"); diff --git a/src/lualib/Delete.ts b/src/lualib/Delete.ts index 079dcb93d..8dfad76ad 100644 --- a/src/lualib/Delete.ts +++ b/src/lualib/Delete.ts @@ -3,17 +3,13 @@ function __TS__Delete(this: void, target: any, key: any): boolean { const descriptor = descriptors[key]; if (descriptor) { if (!descriptor.configurable) { - throw `Cannot delete property ${key} of ${target}.`; + throw new TypeError(`Cannot delete property ${key} of ${target}.`); } descriptors[key] = undefined; return true; } - if (target[key] !== undefined) { - target[key] = undefined; - return true; - } - - return false; + target[key] = undefined; + return true; } diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 2e0899833..9671f656c 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -21,7 +21,7 @@ backQuoteInTemplateString = \\"\` \` \`\\" escapedCharsInQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" escapedCharsInDoubleQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" escapedCharsInTemplateString = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" -nonEmptyTemplateString = (\\"Level 0: \\\\n\\\\t \\" .. ((\\"Level 1: \\\\n\\\\t\\\\t \\" .. ((\\"Level 3: \\\\n\\\\t\\\\t\\\\t \\" .. \\"Last level \\\\n --\\") .. \\" \\\\n --\\")) .. \\" \\\\n --\\")) .. \\" \\\\n --\\"" +nonEmptyTemplateString = (\\"Level 0: \\\\n\\\\t \\" .. (\\"Level 1: \\\\n\\\\t\\\\t \\" .. (\\"Level 3: \\\\n\\\\t\\\\t\\\\t \\" .. \\"Last level \\\\n --\\") .. \\" \\\\n --\\") .. \\" \\\\n --\\") .. \\" \\\\n --\\"" `; exports[`Transformation (exportStatement) 1`] = ` @@ -230,6 +230,30 @@ return ____exports" exports[`Transformation (modulesVariableNoExport) 1`] = `"foo = \\"bar\\""`; +exports[`Transformation (printFormat) 1`] = ` +"stringConcat = ((\\"a\\" .. \\"b\\" .. \\"c\\") .. \\"d\\") .. \\"e\\" +numbers = 2 * 2 + 3 + 4 * (5 + 6) ~= 7 +function func(...) +end +func(function() + local b = \\"A function\\" +end) +func(func()) +array = {func()} +array2 = { + func(), + func() +} +object = {a = 1, b = 2, c = 3} +bigObject = { + a = 1, + b = 2, + c = 3, + d = \\"value1\\", + e = \\"value2\\" +}" +`; + exports[`Transformation (returnDefault) 1`] = ` "function myFunc(self) return diff --git a/test/translation/transformation/printFormat.ts b/test/translation/transformation/printFormat.ts new file mode 100644 index 000000000..3374952c5 --- /dev/null +++ b/test/translation/transformation/printFormat.ts @@ -0,0 +1,27 @@ +const stringConcat = "a" + ("b" + "c") + "d" + "e"; +const numbers = 2 * 2 + 3 + 4 * (5 + 6) !== 7; + +function func(this: void, ...args: any) {} + +func(() => { + const b = "A function"; +}); + +func(func()); + +const array = [func()]; +const array2 = [func(), func()]; + +const object = { + a: 1, + b: 2, + c: 3, +}; + +const bigObject = { + a: 1, + b: 2, + c: 3, + d: "value1", + e: "value2", +}; diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 36a0cb24e..91bc2306b 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -14,13 +14,13 @@ return ____exports" exports[`Binary expressions ordering parentheses ("1*(3+4*2)") 1`] = ` "local ____exports = {} -____exports.__result = 1 * (3 + (4 * 2)) +____exports.__result = 1 * (3 + 4 * 2) return ____exports" `; exports[`Binary expressions ordering parentheses ("1*30+4") 1`] = ` "local ____exports = {} -____exports.__result = (1 * 30) + 4 +____exports.__result = 1 * 30 + 4 return ____exports" `; diff --git a/test/unit/__snapshots__/spread.spec.ts.snap b/test/unit/__snapshots__/spread.spec.ts.snap index 0b49fd80a..d27ea931d 100644 --- a/test/unit/__snapshots__/spread.spec.ts.snap +++ b/test/unit/__snapshots__/spread.spec.ts.snap @@ -90,9 +90,7 @@ function ____exports.__main(self) end return test( nil, - { - fn = function(____, arg) return arg end - }, + {fn = function(____, arg) return arg end}, \\"foobar\\" ) end @@ -108,11 +106,9 @@ function ____exports.__main(self) end local function test(self, ...) do - pcall( - function() - error(\\"foobar\\", 0) - end - ) + pcall(function() + error(\\"foobar\\", 0) + end) do return pick(nil, ...) end diff --git a/test/unit/__snapshots__/switch.spec.ts.snap b/test/unit/__snapshots__/switch.spec.ts.snap index 1349374bb..45ba87e16 100644 --- a/test/unit/__snapshots__/switch.spec.ts.snap +++ b/test/unit/__snapshots__/switch.spec.ts.snap @@ -50,14 +50,14 @@ function ____exports.__main(self) result = hoisted(nil) break end - ____cond3 = ____cond3 or (____switch3 == 2) + ____cond3 = ____cond3 or ____switch3 == 2 if ____cond3 then result = \\"2\\" end if ____cond3 then result = \\"default\\" end - ____cond3 = ____cond3 or (____switch3 == 3) + ____cond3 = ____cond3 or ____switch3 == 3 if ____cond3 then result = \\"3\\" break @@ -88,14 +88,14 @@ function ____exports.__main(self) result = hoisted(nil) break end - ____cond3 = ____cond3 or (____switch3 == 2) + ____cond3 = ____cond3 or ____switch3 == 2 if ____cond3 then result = \\"2\\" end if ____cond3 then result = \\"default\\" end - ____cond3 = ____cond3 or (____switch3 == 3) + ____cond3 = ____cond3 or ____switch3 == 3 if ____cond3 then result = \\"3\\" break @@ -118,19 +118,19 @@ function ____exports.__main(self) local out = {} repeat local ____switch3 = 0 - local ____cond3 = ((____switch3 == 0) or (____switch3 == 1)) or (____switch3 == 2) + local ____cond3 = ____switch3 == 0 or ____switch3 == 1 or ____switch3 == 2 if ____cond3 then __TS__ArrayPush(out, \\"0,1,2\\") break end - ____cond3 = ____cond3 or (____switch3 == 3) + ____cond3 = ____cond3 or ____switch3 == 3 if ____cond3 then do __TS__ArrayPush(out, \\"3\\") break end end - ____cond3 = ____cond3 or (____switch3 == 4) + ____cond3 = ____cond3 or ____switch3 == 4 if ____cond3 then break end diff --git a/test/unit/builtins/__snapshots__/console.spec.ts.snap b/test/unit/builtins/__snapshots__/console.spec.ts.snap index f06d99dae..2af4f0446 100644 --- a/test/unit/builtins/__snapshots__/console.spec.ts.snap +++ b/test/unit/builtins/__snapshots__/console.spec.ts.snap @@ -57,9 +57,7 @@ return ____exports" exports[`console.error ("console.error(\\"Hello %%s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %%s\\", \\"there\\") - ) + print(string.format(\\"Hello %%s\\", \\"there\\")) end return ____exports" `; @@ -67,9 +65,7 @@ return ____exports" exports[`console.error ("console.error(\\"Hello %s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %s\\", \\"there\\") - ) + print(string.format(\\"Hello %s\\", \\"there\\")) end return ____exports" `; @@ -101,9 +97,7 @@ return ____exports" exports[`console.info ("console.info(\\"Hello %%s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %%s\\", \\"there\\") - ) + print(string.format(\\"Hello %%s\\", \\"there\\")) end return ____exports" `; @@ -111,9 +105,7 @@ return ____exports" exports[`console.info ("console.info(\\"Hello %s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %s\\", \\"there\\") - ) + print(string.format(\\"Hello %s\\", \\"there\\")) end return ____exports" `; @@ -145,9 +137,7 @@ return ____exports" exports[`console.log ("console.log(\\"Hello %%s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %%s\\", \\"there\\") - ) + print(string.format(\\"Hello %%s\\", \\"there\\")) end return ____exports" `; @@ -155,9 +145,7 @@ return ____exports" exports[`console.log ("console.log(\\"Hello %s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %s\\", \\"there\\") - ) + print(string.format(\\"Hello %s\\", \\"there\\")) end return ____exports" `; @@ -181,9 +169,7 @@ return ____exports" exports[`console.trace ("console.trace()") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - debug.traceback() - ) + print(debug.traceback()) end return ____exports" `; @@ -191,11 +177,7 @@ return ____exports" exports[`console.trace ("console.trace(\\"Hello %%s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - debug.traceback( - string.format(\\"Hello %%s\\", \\"there\\") - ) - ) + print(debug.traceback(string.format(\\"Hello %%s\\", \\"there\\"))) end return ____exports" `; @@ -203,11 +185,7 @@ return ____exports" exports[`console.trace ("console.trace(\\"Hello %s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - debug.traceback( - string.format(\\"Hello %s\\", \\"there\\") - ) - ) + print(debug.traceback(string.format(\\"Hello %s\\", \\"there\\"))) end return ____exports" `; @@ -215,9 +193,7 @@ return ____exports" exports[`console.trace ("console.trace(\\"Hello\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - debug.traceback(\\"Hello\\", \\"there\\") - ) + print(debug.traceback(\\"Hello\\", \\"there\\")) end return ____exports" `; @@ -225,9 +201,7 @@ return ____exports" exports[`console.trace ("console.trace(\\"message\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - debug.traceback(\\"message\\") - ) + print(debug.traceback(\\"message\\")) end return ____exports" `; @@ -243,9 +217,7 @@ return ____exports" exports[`console.warn ("console.warn(\\"Hello %%s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %%s\\", \\"there\\") - ) + print(string.format(\\"Hello %%s\\", \\"there\\")) end return ____exports" `; @@ -253,9 +225,7 @@ return ____exports" exports[`console.warn ("console.warn(\\"Hello %s\\", \\"there\\")") 1`] = ` "local ____exports = {} function ____exports.__main(self) - print( - string.format(\\"Hello %s\\", \\"there\\") - ) + print(string.format(\\"Hello %s\\", \\"there\\")) end return ____exports" `; diff --git a/test/unit/builtins/object.spec.ts b/test/unit/builtins/object.spec.ts index fb0fe26ba..9cb5f8de3 100644 --- a/test/unit/builtins/object.spec.ts +++ b/test/unit/builtins/object.spec.ts @@ -222,11 +222,15 @@ describe("delete from object", () => { test("delete from object", () => { util.testFunction` const obj = { foo: "bar", bar: "baz" }; - delete obj["foo"]; - return obj; - ` - .setTsHeader("declare function setmetatable(this: void, table: T, metatable: any): T;") - .expectToEqual({ bar: "baz" }); + return [delete obj["foo"], obj]; + `.expectToMatchJsResult(); + }); + + test("delete nonexistent property", () => { + util.testFunction` + const obj = {} as any + return [delete obj["foo"], obj]; + `.expectToMatchJsResult(); }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/993 @@ -234,10 +238,29 @@ describe("delete from object", () => { util.testFunction` const obj = { foo: "bar", bar: "baz" }; setmetatable(obj, {}); - delete obj["foo"]; - return obj; + return [delete obj["foo"], obj]; ` .setTsHeader("declare function setmetatable(this: void, table: T, metatable: any): T;") - .expectToEqual({ bar: "baz" }); + .expectToEqual([true, { bar: "baz" }]); + }); + + test("delete nonconfigurable own property", () => { + util.testFunction` + const obj = {} as any + Object.defineProperty(obj, "foo", { + configurable: false, + value: true + }) + try { + delete obj["foo"] + return "deleted" + } catch (e) { + return e instanceof TypeError + } + ` + .setOptions({ + strict: true, + }) + .expectToMatchJsResult(); }); }); diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index e3f15be29..6bd3922eb 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -20,9 +20,7 @@ function ____exports.__main(self) local function multi(self, ...) return ... end - return ({ - multi(nil) - }).forEach + return ({multi(nil)}).forEach end return ____exports" `; @@ -42,9 +40,7 @@ exports[`invalid $multi call ($multi): diagnostics 1`] = `"main.ts(2,9): error T exports[`invalid $multi call (([a] = $multi(1)) => {}): code 1`] = ` "local function ____(____, ____bindingPattern0) if ____bindingPattern0 == nil then - ____bindingPattern0 = { - ____(_G, 1) - } + ____bindingPattern0 = {____(_G, 1)} end local a = ____bindingPattern0[1] end" @@ -65,21 +61,11 @@ end" exports[`invalid $multi call (const [a = 0] = $multi()): diagnostics 1`] = `"main.ts(2,25): error TSTL: The $multi function must be called in a return statement."`; -exports[`invalid $multi call (const {} = $multi();): code 1`] = ` -"local _____24multi_result_0 = { - { - ____(_G) - } -}" -`; +exports[`invalid $multi call (const {} = $multi();): code 1`] = `"local _____24multi_result_0 = {{____(_G)}}"`; exports[`invalid $multi call (const {} = $multi();): diagnostics 1`] = `"main.ts(2,20): error TSTL: The $multi function must be called in a return statement."`; -exports[`invalid $multi call (const a = $multi();): code 1`] = ` -"a = { - ____(_G) -}" -`; +exports[`invalid $multi call (const a = $multi();): code 1`] = `"a = {____(_G)}"`; exports[`invalid $multi call (const a = $multi();): diagnostics 1`] = `"main.ts(2,19): error TSTL: The $multi function must be called in a return statement."`; @@ -180,9 +166,7 @@ local function multi(self, ...) return ... end local a -local ____temp_0 = { - ____(nil) -} +local ____temp_0 = {____(nil)} a = ____temp_0[1] ____exports.a = a ____exports.a = a @@ -198,9 +182,7 @@ local function multi(self, ...) end local a do - local ____temp_0 = { - ____(nil, 1, 2) - } + local ____temp_0 = {____(nil, 1, 2)} a = ____temp_0[1] ____exports.a = a while false do @@ -237,9 +219,7 @@ local function multi(self, ...) return ... end local a -local _____24multi_result_0 = { - ____(nil, 1) -} +local _____24multi_result_0 = {____(nil, 1)} a = _____24multi_result_0[1] ____exports.a = a if _____24multi_result_0 then diff --git a/test/unit/language-extensions/__snapshots__/range.spec.ts.snap b/test/unit/language-extensions/__snapshots__/range.spec.ts.snap index f066c1260..c1f369cb6 100644 --- a/test/unit/language-extensions/__snapshots__/range.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/range.spec.ts.snap @@ -36,11 +36,7 @@ exports[`$range invalid use ("const y = [...$range(1, 10)];"): code 1`] = ` "require(\\"lualib_bundle\\"); local ____exports = {} function ____exports.__main(self) - local y = { - __TS__Spread( - ____(nil, 1, 10) - ) - } + local y = {__TS__Spread(____(nil, 1, 10))} end return ____exports" `; @@ -50,9 +46,7 @@ exports[`$range invalid use ("const y = [...$range(1, 10)];"): diagnostics 1`] = exports[`$range invalid use ("for (const i in $range(1, 10, 2)) {}"): code 1`] = ` "local ____exports = {} function ____exports.__main(self) - for i in pairs( - ____(nil, 1, 10, 2) - ) do + for i in pairs(____(nil, 1, 10, 2)) do end end return ____exports" @@ -63,9 +57,7 @@ exports[`$range invalid use ("for (const i in $range(1, 10, 2)) {}"): diagnostic exports[`$range invalid use ("for (const i of $range(1, 10, 2) as number[]) {}"): code 1`] = ` "local ____exports = {} function ____exports.__main(self) - for ____, i in ipairs( - ____(nil, 1, 10, 2) - ) do + for ____, i in ipairs(____(nil, 1, 10, 2)) do end end return ____exports" @@ -77,9 +69,7 @@ exports[`$range invalid use ("for (const i of ($range(1, 10, 2))) {}"): code 1`] "require(\\"lualib_bundle\\"); local ____exports = {} function ____exports.__main(self) - for ____, i in __TS__Iterator( - ____(nil, 1, 10, 2) - ) do + for ____, i in __TS__Iterator(____(nil, 1, 10, 2)) do end end return ____exports"