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/LuaLib.ts b/src/LuaLib.ts index 03b3e4ef7..f22ff28e1 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..2370871ca --- /dev/null +++ b/src/lualib/SparseArrayNew.ts @@ -0,0 +1,9 @@ +type __TS__SparseArray = T[] & { sparseLength: number }; + +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 new file mode 100644 index 000000000..30704620f --- /dev/null +++ b/src/lualib/SparseArrayPush.ts @@ -0,0 +1,8 @@ +function __TS__SparseArrayPush(this: void, sparseArray: __TS__SparseArray, ...args: T[]): void { + const argsLen = select("#", ...args); + const listLen = sparseArray.sparseLength; + for (const i of $range(1, argsLen)) { + sparseArray[listLen + i - 1] = args[i - 1]; + } + sparseArray.sparseLength = listLen + argsLen; +} diff --git a/src/lualib/SparseArraySpread.ts b/src/lualib/SparseArraySpread.ts new file mode 100644 index 000000000..e365f19bc --- /dev/null +++ b/src/lualib/SparseArraySpread.ts @@ -0,0 +1,4 @@ +function __TS__SparseArraySpread(this: void, sparseArray: __TS__SparseArray): LuaMultiReturn { + const _unpack = unpack ?? table.unpack; + return _unpack(sparseArray, 1, sparseArray.sparseLength); +} 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 952d18c7e..c71f855fc 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/context/context.ts b/src/transformation/context/context.ts index 4dbcf9e10..30f274c03 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -1,11 +1,14 @@ 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 { createSafeName } from "../utils/safe-names"; import { ExpressionLikeNode, ObjectVisitor, StatementLikeNode, VisitorMap } from "./visitors"; +export const tempSymbolId = -1 as lua.SymbolId; + export interface AllAccessorDeclarations { firstAccessor: ts.AccessorDeclaration; secondAccessor: ts.AccessorDeclaration | undefined; @@ -29,6 +32,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 +50,7 @@ export class TransformationContext { } private currentNodeVisitors: Array> = []; + private nextTempId = 0; public transformNode(node: ts.Node): lua.Node[]; /** @internal */ @@ -104,10 +109,107 @@ 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[]); + return castArray(node).flatMap(n => { + this.pushPrecedingStatements(); + const statements = this.superTransformNode(n) as lua.Statement[]; + statements.unshift(...this.popPrecedingStatements()); + return statements; + }); + } + + public pushPrecedingStatements() { + this.precedingStatementsStack.push([]); + } + + public popPrecedingStatements() { + const precedingStatements = this.precedingStatementsStack.pop(); + assert(precedingStatements); + return precedingStatements; + } + + public addPrecedingStatements(statements: lua.Statement | lua.Statement[]) { + const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; + assert(precedingStatements); + if (!Array.isArray(statements)) { + statements = [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 because createSafeName will add them again + return createSafeName(`${prefix}_${this.nextTempId++}`); + } + + private getTempNameForLuaExpression(expression: lua.Expression): string | undefined { + 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)) { + const name = this.getTempNameForLuaExpression(expression.expression); + if (name) { + return `${name}_result`; + } + } else if (lua.isTableIndexExpression(expression)) { + const tableName = this.getTempNameForLuaExpression(expression.table); + const indexName = this.getTempNameForLuaExpression(expression.index); + if (tableName || indexName) { + return `${tableName ?? "table"}_${indexName ?? "index"}`; + } + } + } + + 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)); + return identifier; + } + + private getTempNameForNode(node: ts.Node): string | undefined { + 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) { + return `${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) { + return `${tableName ?? "table"}_${indexName ?? "index"}`; + } + } + } + + 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 091cd73f4..0670c51fc 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"; @@ -62,19 +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( - 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); -} - export function createUnpackCall( context: TransformationContext, expression: lua.Expression, @@ -119,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; @@ -176,22 +158,25 @@ export function createLocalOrExportedOrGlobalDeclaration( if (context.isModule || !isTopLevelVariable) { if (!isFunctionDeclaration && hasMultipleReferences(scope, lhs)) { - // Split declaration and assignment of identifiers that reference themselves in their declaration - declaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal); + // 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.prependPrecedingStatements(precedingDeclaration); 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); } } else if (rhs) { // global 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/scope.ts b/src/transformation/utils/scope.ts index bed90da76..3a4558b4d 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -98,6 +98,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/utils/transform.ts b/src/transformation/utils/transform.ts deleted file mode 100644 index 215a018d5..000000000 --- a/src/transformation/utils/transform.ts +++ /dev/null @@ -1,22 +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.CallExpression { - pushScope(context, ScopeType.Function); - const { statements, result } = transformFunction(); - popScope(context); - return createImmediatelyInvokedFunctionExpression(castArray(statements), result, tsOriginal); -} 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/access.ts b/src/transformation/visitors/access.ts index 389bbde5f..a3ec68c46 100644 --- a/src/transformation/visitors/access.ts +++ b/src/transformation/visitors/access.ts @@ -8,39 +8,45 @@ 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( +function addOneToArrayAccessArgument( 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 transformElementAccessArgument( + context: TransformationContext, + node: ts.ElementAccessExpression +): lua.Expression { + const index = context.transformExpression(node.argumentExpression); + return addOneToArrayAccessArgument(context, node, index); +} + export const transformElementAccessExpression: FunctionVisitor = (node, context) => { const constEnumValue = tryGetConstEnumValue(context, node); if (constEnumValue) { return constEnumValue; } - const table = context.transformExpression(node.expression); + const [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); + const updatedAccessExpression = addOneToArrayAccessArgument(context, node, accessExpression); if (isMultiReturnCall(context, node.expression)) { const accessType = context.checker.getTypeAtLocation(node.argumentExpression); @@ -53,16 +59,22 @@ 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 b934ca2e0..aec1ee96c 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -7,20 +7,33 @@ import { createExportedIdentifier, getDependenciesOfSymbol, isSymbolExported } f 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 { - ImmediatelyInvokedFunctionParameters, - transformToImmediatelyInvokedFunctionExpression, -} from "../../utils/transform"; 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, - node: ts.Expression + node: ts.Expression, + rightHasPrecedingStatements?: boolean ): lua.AssignmentLeftHandSideExpression { + // 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); + 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); + } + return lua.createTableIndexExpression(table, index, node); + } + const symbol = context.checker.getSymbolAtLocation(node); const left = context.transformExpression(node); @@ -34,6 +47,7 @@ export function transformAssignment( // TODO: Change type to ts.LeftHandSideExpression? lhs: ts.Expression, right: lua.Expression, + rightHasPrecedingStatements?: boolean, parent?: ts.Expression ): lua.Statement[] { if (ts.isOptionalChain(lhs)) { @@ -55,13 +69,14 @@ 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); + const left = transformAssignmentLeftHandSideExpression(context, lhs, rightHasPrecedingStatements); const rootAssignment = lua.createAssignmentStatement(left, right, lhs.parent); @@ -75,20 +90,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 -): ImmediatelyInvokedFunctionParameters { - const rootIdentifier = lua.createAnonymousIdentifier(expression.left); +) { + const rootIdentifier = context.createTempNameForNode(expression.right); - let right = context.transformExpression(expression.right); + 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), + ...transformDestructuringAssignment(context, expression, rootIdentifier, rightPrecedingStatements.length > 0), ]; return { statements, result: rootIdentifier }; @@ -115,56 +146,36 @@ 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)) { - // 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.createTempNameForNode(expression.right); + const [precedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); - const args = [objExpression, indexExpression, context.transformExpression(expression.right)]; - popScope(context); - return lua.createCallExpression(iife, args, expression); - } else { - return transformToImmediatelyInvokedFunctionExpression( + const left = transformAssignmentLeftHandSideExpression( 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 + expression.left, + precedingStatements.length > 0 ); + + context.addPrecedingStatements([ + ...precedingStatements, + 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} + const left = context.transformExpression(expression.left); + const right = context.transformExpression(expression.right); + context.addPrecedingStatements(transformAssignment(context, expression.left, right)); + return left; } } @@ -179,7 +190,9 @@ const canBeTransformedToLuaAssignmentStatement = ( } if (ts.isPropertyAccessExpression(element) || ts.isElementAccessExpression(element)) { - return true; + // 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; } if (ts.isIdentifier(element)) { @@ -203,12 +216,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 = transformExpressionList(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)); @@ -216,17 +232,28 @@ export function transformAssignmentStatement( return [lua.createAssignmentStatement(left, right, expression)]; } - let right = context.transformExpression(expression.right); + let [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); + context.addPrecedingStatements(rightPrecedingStatements); if (isMultiReturnCall(context, expression.right)) { right = wrapInTable(right); } - const rootIdentifier = lua.createAnonymousIdentifier(expression.left); + const rootIdentifier = context.createTempNameForNode(expression.left); return [ lua.createVariableDeclarationStatement(rootIdentifier, right), - ...transformDestructuringAssignment(context, expression, rootIdentifier), + ...transformDestructuringAssignment( + context, + expression, + rootIdentifier, + rightPrecedingStatements.length > 0 + ), ]; } else { - return transformAssignment(context, expression.left, context.transformExpression(expression.right)); + 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 febd7681e..76947ede9 100644 --- a/src/transformation/visitors/binary-expression/compound.ts +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -2,38 +2,23 @@ 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 { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; import { transformBinaryOperation } from "../binary-expression"; -import { transformAssignment } from "./assignments"; +import { transformAssignmentWithRightPrecedingStatements } 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 @@ -85,74 +70,136 @@ export function transformCompoundAssignment( rhs: ts.Expression, operator: CompoundAssignmentToken, isPostfix: boolean -): ImmediatelyInvokedFunctionParameters { +) { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); - const right = context.transformExpression(rhs); + const [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(rhs) + ); - 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 = lua.createIdentifier("____obj"); - const index = lua.createIdentifier("____index"); - const objAndIndexDeclaration = lua.createVariableDeclarationStatement( - [obj, index], - [context.transformExpression(objExpression), context.transformExpression(indexExpression)] - ); + 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 = lua.createIdentifier("____tmp"); - let tmpDeclaration: lua.VariableDeclarationStatement; - let assignStatement: lua.AssignmentStatement; + const tmp = context.createTempNameForLuaExpression(left); if (isPostfix) { // local ____tmp = ____obj[____index]; // ____obj[____index] = ____tmp ${replacementOperator} ${right}; - tmpDeclaration = lua.createVariableDeclarationStatement(tmp, accessExpression); - const operatorExpression = transformBinaryOperation(context, tmp, right, operator, expression); - assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); + // return ____tmp + const tmpDeclaration = lua.createVariableDeclarationStatement(tmp, accessExpression); + const [precedingStatements, operatorExpression] = transformBinaryOperation( + context, + tmp, + right, + rightPrecedingStatements, + operator, + expression + ); + const assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); + return { + statements: [objAndIndexDeclaration, ...precedingStatements, tmpDeclaration, assignStatement], + result: tmp, + }; } else { // local ____tmp = ____obj[____index] ${replacementOperator} ${right}; // ____obj[____index] = ____tmp; - const operatorExpression = transformBinaryOperation(context, accessExpression, right, operator, expression); - tmpDeclaration = lua.createVariableDeclarationStatement(tmp, operatorExpression); - assignStatement = lua.createAssignmentStatement(accessExpression, tmp); + // return ____tmp + const [precedingStatements, operatorExpression] = transformBinaryOperation( + context, + accessExpression, + right, + rightPrecedingStatements, + operator, + expression + ); + 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, tmpDeclaration, assignStatement], result: tmp }; } else if (isPostfix) { // Postfix expressions need to cache original value in temp // local ____tmp = ${left}; // ${left} = ____tmp ${replacementOperator} ${right}; // return ____tmp - const tmpIdentifier = lua.createIdentifier("____tmp"); + const tmpIdentifier = context.createTempNameForLuaExpression(left); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); - const operatorExpression = transformBinaryOperation(context, tmpIdentifier, right, operator, expression); - const assignStatements = transformAssignment(context, lhs, operatorExpression); - return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; + const [precedingStatements, operatorExpression] = transformBinaryOperation( + context, + tmpIdentifier, + right, + rightPrecedingStatements, + operator, + expression + ); + const assignStatements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + rightPrecedingStatements + ); + 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 = lua.createIdentifier("____tmp"); - const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); + const tmpIdentifier = context.createTempNameForLuaExpression(left); + const [precedingStatements, operatorExpression] = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + expression + ); const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, operatorExpression); - const assignStatements = transformAssignment(context, lhs, tmpIdentifier); if (isSetterSkippingCompoundAssignmentOperator(operator)) { const statements = [ tmpDeclaration, - ...transformSetterSkippingCompoundAssignment(context, tmpIdentifier, operator, rhs), + ...transformSetterSkippingCompoundAssignment(tmpIdentifier, operator, right, precedingStatements), ]; return { statements, result: tmpIdentifier }; } + const assignStatements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + tmpIdentifier, + precedingStatements + ); return { statements: [tmpDeclaration, ...assignStatements], result: tmpIdentifier }; } else { + if (rightPrecedingStatements.length > 0 && isSetterSkippingCompoundAssignmentOperator(operator)) { + return { + statements: transformSetterSkippingCompoundAssignment(left, operator, right, rightPrecedingStatements), + result: left, + }; + } + // Simple expressions - // ${left} = ${right}; return ${right} - const operatorExpression = transformBinaryOperation(context, left, right, operator, expression); - const statements = transformAssignment(context, lhs, operatorExpression); + // ${left} = ${left} ${operator} ${right} + const [precedingStatements, operatorExpression] = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + expression + ); + const statements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + precedingStatements + ); return { statements, result: left }; } } @@ -165,12 +212,10 @@ export function transformCompoundAssignmentExpression( rhs: ts.Expression, operator: CompoundAssignmentToken, isPostfix: boolean -): lua.CallExpression { - return transformToImmediatelyInvokedFunctionExpression( - context, - () => transformCompoundAssignment(context, expression, lhs, rhs, operator, isPostfix), - expression - ); +): lua.Expression { + const { statements, result } = transformCompoundAssignment(context, expression, lhs, rhs, operator, isPostfix); + context.addPrecedingStatements(statements); + return result; } export function transformCompoundAssignmentStatement( @@ -181,41 +226,66 @@ export function transformCompoundAssignmentStatement( operator: CompoundAssignmentToken ): lua.Statement[] { const left = cast(context.transformExpression(lhs), lua.isAssignmentLeftHandSideExpression); - const right = context.transformExpression(rhs); + let [rightPrecedingStatements, right] = transformInPrecedingStatementScope(context, () => + context.transformExpression(rhs) + ); - 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.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); if (isSetterSkippingCompoundAssignmentOperator(operator)) { return [ objAndIndexDeclaration, - ...transformSetterSkippingCompoundAssignment(context, accessExpression, operator, rhs, node), + ...transformSetterSkippingCompoundAssignment( + accessExpression, + operator, + right, + rightPrecedingStatements, + node + ), ]; } - 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, assignStatement]; + return [objAndIndexDeclaration, ...rightPrecedingStatements, 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, rightPrecedingStatements, node); } // Simple statements // ${left} = ${left} ${replacementOperator} ${right} - const operatorExpression = transformBinaryOperation(context, left, right, operator, node); - return transformAssignment(context, lhs, operatorExpression); + let operatorExpression: lua.Expression; + [rightPrecedingStatements, operatorExpression] = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + node + ); + return transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + rightPrecedingStatements + ); } } @@ -237,10 +307,10 @@ function isSetterSkippingCompoundAssignmentOperator( } function transformSetterSkippingCompoundAssignment( - context: TransformationContext, lhs: lua.AssignmentLeftHandSideExpression, operator: SetterSkippingCompoundAssignmentOperator, - rhs: ts.Expression, + 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. @@ -260,7 +330,7 @@ function transformSetterSkippingCompoundAssignment( return [ lua.createIfStatement( condition, - lua.createBlock([lua.createAssignmentStatement(lhs, context.transformExpression(rhs))]), + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(lhs, right, node)]), undefined, node ), diff --git a/src/transformation/visitors/binary-expression/destructuring-assignments.ts b/src/transformation/visitors/binary-expression/destructuring-assignments.ts index 567de4e6f..d3d0086c0 100644 --- a/src/transformation/visitors/binary-expression/destructuring-assignments.ts +++ b/src/transformation/visitors/binary-expression/destructuring-assignments.ts @@ -1,9 +1,12 @@ 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 { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; import { isArrayType, isAssignmentPattern } from "../../utils/typescript"; +import { moveToPrecedingTemp } from "../expression-list"; import { transformPropertyName } from "../literal"; import { transformAssignment, @@ -36,28 +39,31 @@ export function isArrayLength( export function transformDestructuringAssignment( context: TransformationContext, node: ts.DestructuringAssignment, - root: lua.Expression + root: lua.Expression, + rightHasPrecedingStatements: boolean ): lua.Statement[] { - return transformAssignmentPattern(context, node.left, root); + return transformAssignmentPattern(context, node.left, root, rightHasPrecedingStatements); } export function transformAssignmentPattern( context: TransformationContext, node: ts.AssignmentPattern, - root: lua.Expression + root: lua.Expression, + rightHasPrecedingStatements: boolean ): lua.Statement[] { switch (node.kind) { case ts.SyntaxKind.ObjectLiteralExpression: - return transformObjectLiteralAssignmentPattern(context, node, root); + return transformObjectLiteralAssignmentPattern(context, node, root, rightHasPrecedingStatements); case ts.SyntaxKind.ArrayLiteralExpression: - return transformArrayLiteralAssignmentPattern(context, node, root); + return transformArrayLiteralAssignmentPattern(context, node, root, rightHasPrecedingStatements); } } function transformArrayLiteralAssignmentPattern( context: TransformationContext, node: ts.ArrayLiteralExpression, - root: lua.Expression + root: lua.Expression, + rightHasPrecedingStatements: boolean ): lua.Statement[] { return node.elements.flatMap((element, index) => { const indexedRoot = lua.createTableIndexExpression(root, lua.createNumericLiteral(index + 1), element); @@ -67,16 +73,18 @@ function transformArrayLiteralAssignmentPattern( return transformObjectLiteralAssignmentPattern( context, element as ts.ObjectLiteralExpression, - indexedRoot + indexedRoot, + rightHasPrecedingStatements ); case ts.SyntaxKind.ArrayLiteralExpression: return transformArrayLiteralAssignmentPattern( context, element as ts.ArrayLiteralExpression, - indexedRoot + indexedRoot, + rightHasPrecedingStatements ); case ts.SyntaxKind.BinaryExpression: - const assignedVariable = lua.createIdentifier("____bindingAssignmentValue"); + const assignedVariable = context.createTempNameForLuaExpression(indexedRoot); const assignedVariableDeclaration = lua.createVariableDeclarationStatement( assignedVariable, @@ -89,12 +97,19 @@ function transformArrayLiteralAssignmentPattern( lua.SyntaxKind.EqualityOperator ); - 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(...defaultPrecedingStatements); + const elseAssignmentStatements = transformAssignment( context, (element as ts.BinaryExpression).left, @@ -111,7 +126,10 @@ function transformArrayLiteralAssignmentPattern( case ts.SyntaxKind.Identifier: case ts.SyntaxKind.PropertyAccessExpression: case ts.SyntaxKind.ElementAccessExpression: - return transformAssignment(context, element, indexedRoot); + 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 @@ -126,7 +144,15 @@ function transformArrayLiteralAssignmentPattern( lua.createNumericLiteral(index) ); - return transformAssignment(context, (element as ts.SpreadElement).expression, restElements); + const [spreadPrecedingStatements, spreadStatements] = transformInPrecedingStatementScope(context, () => + transformAssignment( + context, + (element as ts.SpreadElement).expression, + restElements, + rightHasPrecedingStatements + ) + ); + return [...spreadPrecedingStatements, ...spreadStatements]; // Keep preceding statements in order case ts.SyntaxKind.OmittedExpression: return []; default: @@ -139,7 +165,8 @@ function transformArrayLiteralAssignmentPattern( function transformObjectLiteralAssignmentPattern( context: TransformationContext, node: ts.ObjectLiteralExpression, - root: lua.Expression + root: lua.Expression, + rightHasPrecedingStatements: boolean ): lua.Statement[] { const result: lua.Statement[] = []; @@ -149,7 +176,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, rightHasPrecedingStatements)); break; case ts.SyntaxKind.SpreadAssignment: result.push(...transformSpreadAssignment(context, property, root, node.properties)); @@ -207,7 +234,8 @@ function transformShorthandPropertyAssignment( function transformPropertyAssignment( context: TransformationContext, node: ts.PropertyAssignment, - root: lua.Expression + root: lua.Expression, + rightHasPrecedingStatements: boolean ): lua.Statement[] { const result: lua.Statement[] = []; @@ -216,38 +244,95 @@ 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, + rightHasPrecedingStatements + ); } if (ts.isArrayLiteralExpression(node.initializer)) { - return transformArrayLiteralAssignmentPattern(context, node.initializer, newRootAccess); + return transformArrayLiteralAssignmentPattern( + context, + node.initializer, + newRootAccess, + rightHasPrecedingStatements + ); } } - const leftExpression = ts.isBinaryExpression(node.initializer) ? node.initializer.left : node.initializer; - const variableToExtract = transformPropertyName(context, node.name); - const extractingExpression = lua.createTableIndexExpression(root, variableToExtract); + context.pushPrecedingStatements(); - const destructureAssignmentStatements = transformAssignment(context, leftExpression, extractingExpression); - - result.push(...destructureAssignmentStatements); + let variableToExtract = transformPropertyName(context, node.name); + // Must be evaluated before left's preceding statements + variableToExtract = moveToPrecedingTemp(context, variableToExtract, node.name); + const extractingExpression = lua.createTableIndexExpression(root, variableToExtract); + let destructureAssignmentStatements: lua.Statement[]; if (ts.isBinaryExpression(node.initializer)) { - 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) + 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); + + const rightExpression = node.initializer.right; + const [defaultPrecedingStatements, defaultExpression] = transformInPrecedingStatementScope(context, () => + context.transformExpression(rightExpression) + ); + + const tableTemp = context.createTempNameForLuaExpression(left.table); + const indexTemp = context.createTempNameForLuaExpression(left.index); + + const tempsDeclaration = lua.createVariableDeclarationStatement( + [tableTemp, indexTemp], + [left.table, left.index] + ); + + // 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), + rhs + ); + + destructureAssignmentStatements = [tempsDeclaration, ...rightPrecedingStatements, 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, + rightHasPrecedingStatements ); - - result.push(lua.createIfStatement(nilCondition, ifBlock, undefined, node)); } + result.push(...context.popPrecedingStatements()); + result.push(...destructureAssignmentStatements); + return result; } diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts index 93ea66018..df4ef6434 100644 --- a/src/transformation/visitors/binary-expression/index.ts +++ b/src/transformation/visitors/binary-expression/index.ts @@ -14,7 +14,18 @@ import { unwrapCompoundAssignmentToken, } from "./compound"; import { assert } from "../../../utils"; -import { transformToImmediatelyInvokedFunctionExpression } from "../../utils/transform"; +import { transformOrderedExpressions } from "../expression-list"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; + +type ShortCircuitOperator = + | ts.SyntaxKind.AmpersandAmpersandToken + | 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 @@ -41,7 +52,7 @@ const simpleOperatorsToLua: Record = { [ts.SyntaxKind.ExclamationEqualsEqualsToken]: lua.SyntaxKind.InequalityOperator, }; -export function transformBinaryOperation( +function transformBinaryOperationWithNoPrecedingStatements( context: TransformationContext, left: lua.Expression, right: lua.Expression, @@ -76,6 +87,100 @@ export function transformBinaryOperation( return lua.createBinaryExpression(left, right, luaOperator, node); } +export function createShortCircuitBinaryExpressionPrecedingStatements( + context: TransformationContext, + lhs: lua.Expression, + rhs: lua.Expression, + rightPrecedingStatements: lua.Statement[], + operator: ShortCircuitOperator, + node?: ts.BinaryExpression +): [lua.Statement[], lua.Expression] { + 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( + condition, + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(conditionIdentifier, rhs)]), + undefined, + node?.left + ); + return [[assignmentStatement, ifStatement], conditionIdentifier]; +} + +function transformShortCircuitBinaryExpression( + context: TransformationContext, + node: ts.BinaryExpression, + 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 createShortCircuitBinaryExpressionPrecedingStatements( + context, + lhs, + rhs, + rightPrecedingStatements, + operator, + node + ); + } else { + 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 && isShortCircuitOperator(operator)) { + assert(ts.isBinaryExpression(node)); + return createShortCircuitBinaryExpressionPrecedingStatements( + context, + left, + right, + rightPrecedingStatements, + operator, + node + ); + } + + return [ + rightPrecedingStatements, + transformBinaryOperationWithNoPrecedingStatements(context, left, right, operator, node), + ]; +} + export const transformBinaryExpression: FunctionVisitor = (node, context) => { const operator = node.operatorToken.kind; @@ -118,25 +223,31 @@ 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)); + const [precedingStatements, result] = transformInPrecedingStatementScope(context, () => + context.transformExpression(node.right) ); + statements.push(...precedingStatements); + context.addPrecedingStatements(statements); + return result; } - default: - return transformBinaryOperation( - context, - context.transformExpression(node.left), - context.transformExpression(node.right), - operator, - node - ); + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.BarBarToken: { + const [precedingStatements, result] = transformShortCircuitBinaryExpression(context, node, operator); + context.addPrecedingStatements(precedingStatements); + return result; + } } + + 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/call.ts b/src/transformation/visitors/call.ts index 22adb9a45..a863789ae 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"; @@ -23,124 +23,107 @@ import { transformTableSetExpression, } from "./language-extensions/table"; import { annotationRemoved, invalidTableDeleteExpression, invalidTableSetExpression } from "../utils/diagnostics"; -import { - ImmediatelyInvokedFunctionParameters, - transformToImmediatelyInvokedFunctionExpression, -} from "../utils/transform"; +import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; 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( +export function validateArguments( 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); + params: readonly ts.Expression[], + signature?: ts.Signature +) { + if (!signature || signature.parameters.length < params.length) { + return; + } + 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 chunks.map(chunk => wrapInTable(...chunk.map(expression => context.transformExpression(expression)))); } -export function flattenSpreadExpressions( +export function transformArguments( context: TransformationContext, - expressions: readonly ts.Expression[] + params: readonly ts.Expression[], + signature?: ts.Signature, + callContext?: ts.Expression ): lua.Expression[] { - const [preSpreadExpressions, postSpreadExpressions] = getExpressionsBeforeAndAfterFirstSpread(expressions); - const transformedPreSpreadExpressions = preSpreadExpressions.map(a => context.transformExpression(a)); + validateArguments(context, params, signature); + return transformExpressionList(context, callContext ? [callContext, ...params] : params); +} - // Nothing special required - if (postSpreadExpressions.length === 0) { - return transformedPreSpreadExpressions; +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); } - // Only one spread element at the end? Will work as expected - if (postSpreadExpressions.length === 1) { - return [...transformedPreSpreadExpressions, context.transformExpression(postSpreadExpressions[0])]; + if (argPrecedingStatements.length > 0) { + if (transformedContext) { + transformedContext = moveToPrecedingTemp(context, transformedContext, callContext); + } + call = moveToPrecedingTemp(context, call, callExpression); + context.addPrecedingStatements(argPrecedingStatements); } - // 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 (transformedContext) { + transformedArguments.unshift(transformedContext); + } - return [...transformedPreSpreadExpressions, lastExpression]; + return [call, transformedArguments]; } -export function transformArguments( +export function transformCallAndArguments( context: TransformationContext, + callExpression: ts.Expression, params: readonly ts.Expression[], signature?: ts.Signature, callContext?: ts.Expression -): lua.Expression[] { - const parameters = flattenSpreadExpressions(context, params); - - // Add context as first param if present - if (callContext) { - parameters.unshift(context.transformExpression(callContext)); - } - - 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); - } - } - } - - return parameters; +): [lua.Expression, lua.Expression[]] { + const [argPrecedingStatements, transformedArguments] = transformInPrecedingStatementScope(context, () => + transformArguments(context, params, signature, callContext) + ); + return transformCallWithArguments(context, callExpression, transformedArguments, argPrecedingStatements); } function transformElementAccessCall( context: TransformationContext, left: ts.PropertyAccessExpression | ts.ElementAccessExpression, - args: ts.Expression[] | ts.NodeArray, - signature?: ts.Signature -): ImmediatelyInvokedFunctionParameters { - const transformedArguments = transformArguments(context, args, signature, ts.factory.createIdentifier("____self")); - + transformedArguments: lua.Expression[], + argPrecedingStatements: lua.Statement[] +) { // Cache left-side if it has effects - // (function() local ____self = context; return ____self[argument](parameters); end)() + // 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); - const selfIdentifier = lua.createIdentifier("____self"); - 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 }; + + let index: lua.Expression = lua.createTableIndexExpression(selfIdentifier, argument); + + if (argPrecedingStatements.length > 0) { + // Cache index in temp if args had preceding statements + index = moveToPrecedingTemp(context, index); + context.addPrecedingStatements(argPrecedingStatements); + } + + return lua.createCallExpression(index, [selfIdentifier, ...transformedArguments]); } export function transformContextualCallExpression( @@ -150,10 +133,19 @@ 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)) { + + let [argPrecedingStatements, transformedArguments] = transformInPrecedingStatementScope(context, () => + transformArguments(context, args, signature) + ); + + 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, @@ -162,33 +154,40 @@ 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 ); } } else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) { if (isExpressionWithEvaluationEffect(left.expression)) { - return transformToImmediatelyInvokedFunctionExpression( + return transformElementAccessCall(context, left, transformedArguments, argPrecedingStatements); + } else { + let expression: lua.Expression; + [expression, transformedArguments] = transformCallWithArguments( context, - () => transformElementAccessCall(context, left, args, signature), - node + left, + transformedArguments, + argPrecedingStatements, + left.expression ); - } else { - const callContext = context.transformExpression(left.expression); - const expression = context.transformExpression(left); - const transformedArguments = transformArguments(context, args, signature); - return lua.createCallExpression(expression, [callContext, ...transformedArguments]); + return lua.createCallExpression(expression, transformedArguments, 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); + let expression: lua.Expression; + [expression, transformedArguments] = transformCallWithArguments( + context, + left, + transformedArguments, + argPrecedingStatements, + callContext + ); return lua.createCallExpression(expression, transformedArguments, node); } else { throw new Error(`Unsupported LeftHandSideExpression kind: ${ts.SyntaxKind[left.kind]}`); @@ -213,8 +212,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); @@ -235,8 +233,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); } } @@ -260,11 +257,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)) { @@ -277,11 +271,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)) { @@ -316,15 +307,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/class/index.ts b/src/transformation/visitors/class/index.ts index 2fb18a0cb..0ea562fa4 100644 --- a/src/transformation/visitors/class/index.ts +++ b/src/transformation/visitors/class/index.ts @@ -11,9 +11,8 @@ import { hasExportModifier, 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"; @@ -46,14 +45,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(); @@ -73,8 +67,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"), classDeclaration); } const annotations = getTypeAnnotations(context.checker.getTypeAtLocation(classDeclaration)); 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/conditional.ts b/src/transformation/visitors/conditional.ts index 2fab7494c..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"; @@ -27,32 +28,34 @@ 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 = context.createTempNameForNode(expression.condition); + const condition = context.transformExpression(expression.condition); - const val1 = context.transformExpression(expression.whenTrue); - const val2 = context.transformExpression(expression.whenFalse); - const val1Function = wrapInFunctionCall(val1); - const val2Function = wrapInFunctionCall(val2); + const [trueStatements, val1] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.whenTrue) + ); + trueStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val1, expression.whenTrue)); - // (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); + const [falseStatements, val2] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.whenFalse) + ); + falseStatements.push(lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), val2, expression.whenFalse)); + + context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(tempVar, undefined, expression.condition), + lua.createIfStatement( + condition, + lua.createBlock(trueStatements, expression.whenTrue), + lua.createBlock(falseStatements, expression.whenFalse), + expression + ), + ]); + return lua.cloneIdentifier(tempVar); } export const transformConditionalExpression: FunctionVisitor = (expression, context) => { @@ -78,8 +81,24 @@ export function transformIfStatement(statement: ts.IfStatement, context: Transfo if (statement.elseStatement) { if (ts.isIfStatement(statement.elseStatement)) { - const elseStatement = transformIfStatement(statement.elseStatement, context); - return lua.createIfStatement(condition, ifBlock, elseStatement); + 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); + } else { + return lua.createIfStatement(condition, ifBlock, elseStatement); + } } else { pushScope(context, ScopeType.Conditional); const elseStatements = performHoisting( diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts new file mode 100644 index 000000000..01213ce4d --- /dev/null +++ b/src/transformation/visitors/expression-list.ts @@ -0,0 +1,192 @@ +import assert = require("assert"); +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) { + return ( + !lua.isLiteral(expression) && + !(lua.isIdentifier(expression) && expression.symbolId === tempSymbolId) && // Treat generated temps as consts + !(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 (!shouldMoveToTemp(context, expression, tsOriginal)) { + return expression; + } + const tempIdentifier = context.createTempNameForLuaExpression(expression); + const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression, tsOriginal); + context.addPrecedingStatements(tempDeclaration); + return lua.cloneIdentifier(tempIdentifier, tsOriginal); +} + +function transformExpressions( + context: TransformationContext, + expressions: readonly ts.Expression[] +): { + transformedExpressions: lua.Expression[]; + precedingStatements: lua.Statement[][]; + lastPrecedingStatementsIndex: number; +} { + const precedingStatements: lua.Statement[][] = []; + const transformedExpressions: lua.Expression[] = []; + let lastPrecedingStatementsIndex = -1; + for (let i = 0; i < expressions.length; ++i) { + const [expressionPrecedingStatements, expression] = transformInPrecedingStatementScope(context, () => + context.transformExpression(expressions[i]) + ); + transformedExpressions.push(expression); + if (expressionPrecedingStatements.length > 0) { + lastPrecedingStatementsIndex = i; + } + precedingStatements.push(expressionPrecedingStatements); + } + return { transformedExpressions, precedingStatements, 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) { + context.addPrecedingStatements(precedingStatements[i]); + if (i < lastPrecedingStatementsIndex) { + transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpressions[i], expressions[i]); + } + } + return transformedExpressions; +} + +function pushToSparseArray( + context: TransformationContext, + arrayIdentifier: lua.Identifier | undefined, + expressions: lua.Expression[] +) { + if (!arrayIdentifier) { + arrayIdentifier = lua.createIdentifier(context.createTempName("array")); + const libCall = transformLuaLibFunction(context, LuaLibFeature.SparseArrayNew, undefined, ...expressions); + const declaration = lua.createVariableDeclarationStatement(arrayIdentifier, libCall); + context.addPrecedingStatements(declaration); + } else { + const libCall = transformLuaLibFunction( + context, + LuaLibFeature.SparseArrayPush, + undefined, + arrayIdentifier, + ...expressions + ); + context.addPrecedingStatements(lua.createExpressionStatement(libCall)); + } + return arrayIdentifier; +} + +function transformExpressionsUsingSparseArray( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + precedingStatements: lua.Statement[][] +) { + let arrayIdentifier: lua.Identifier | undefined; + + let expressionBatch: lua.Expression[] = []; + for (let i = 0; i < expressions.length; ++i) { + // 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]); + expressionBatch.push(transformedExpressions[i]); + + // Spread expressions should always be at the end of a batch + if (ts.isSpreadElement(expressions[i])) { + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); + expressionBatch = []; + } + } + + if (expressionBatch.length > 0) { + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); + } + + assert(arrayIdentifier); + return [transformLuaLibFunction(context, LuaLibFeature.SparseArraySpread, undefined, arrayIdentifier)]; +} + +function countNeededTemps( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + lastPrecedingStatementsIndex: number +) { + if (lastPrecedingStatementsIndex < 0) { + return 0; + } + return transformedExpressions + .slice(0, lastPrecedingStatementsIndex) + .filter((e, i) => shouldMoveToTemp(context, e, expressions[i])).length; +} + +// Transforms a list of expressions while flattening spreads and maintaining execution order +export function transformExpressionList( + context: TransformationContext, + expressions: readonly ts.Expression[] +): lua.Expression[] { + 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 + // sparse array lib functions instead to prevent excessive locals. + const maxTemps = 2; + + // 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) || + countNeededTemps(context, expressions, transformedExpressions, lastPrecedingStatementsIndex) > maxTemps + ) { + return transformExpressionsUsingSparseArray(context, expressions, transformedExpressions, precedingStatements); + } else { + return transformExpressionsUsingTemps( + context, + expressions, + transformedExpressions, + precedingStatements, + lastPrecedingStatementsIndex + ); + } +} + +// 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/function.ts b/src/transformation/visitors/function.ts index 166a1b87a..dd749d202 100644 --- a/src/transformation/visitors/function.ts +++ b/src/transformation/visitors/function.ts @@ -8,12 +8,12 @@ import { createDefaultExportStringLiteral, hasDefaultExportModifier } from "../u import { ContextType, getFunctionContextType } from "../utils/function-context"; import { createExportsIdentifier, - createImmediatelyInvokedFunctionExpression, createLocalOrExportedOrGlobalDeclaration, createSelfIdentifier, 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"; @@ -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)) { - const returnStatement = transformExpressionBodyToReturnStatement(context, body); - return [returnStatement]; + const [precedingStatements, returnStatement] = transformInPrecedingStatementScope(context, () => + transformExpressionBodyToReturnStatement(context, body) + ); + return [...precedingStatements, returnStatement]; } const bodyStatements = performHoisting(context, context.transformStatements(body.statements)); @@ -242,17 +244,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. - return createImmediatelyInvokedFunctionExpression( - [lua.createVariableDeclarationStatement(nameIdentifier, functionExpression)], - lua.cloneIdentifier(nameIdentifier) + context.addPrecedingStatements( + lua.createVariableDeclarationStatement(nameIdentifier, functionExpression) ); + return lua.cloneIdentifier(nameIdentifier); } } } diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts index c0e9a6256..32576a815 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 { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; import { findMultiAssignmentViolations } from "./language-extensions/multi"; // TODO: Move to object-literal.ts? @@ -69,15 +69,33 @@ const transformObjectLiteralExpression: FunctionVisitor 0) { + lastPrecedingStatementsIndex = i; + } + + // Transform value and cache preceding statements + context.pushPrecedingStatements(); + if (ts.isPropertyAssignment(element)) { const expression = context.transformExpression(element.initializer); properties.push(lua.createTableFieldExpression(expression, name, element)); + initializers.push(element.initializer); } else if (ts.isShorthandPropertyAssignment(element)) { const valueSymbol = context.checker.getShorthandAssignmentValueSymbol(element); if (valueSymbol) { @@ -86,18 +104,12 @@ const transformObjectLiteralExpression: 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)) { @@ -111,26 +123,74 @@ const transformObjectLiteralExpression: 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 < properties.length; ++i) { + const property = properties[i]; + + // Bubble up key's preceding statements + context.addPrecedingStatements(keyPrecedingStatements[i]); + + // Cache computed property name in temp if before the last expression that generated preceding statements + if (i <= lastPrecedingStatementsIndex && lua.isTableFieldExpression(property) && property.key) { + property.key = moveToPrecedingTemp(context, property.key, expression.properties[i].name); + } + + // Bubble up value's preceding statements + context.addPrecedingStatements(valuePrecedingStatements[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]); + } + } + } + } + + // Sort into field expressions and tables to pass into __TS__ObjectAssign + let fields: lua.TableFieldExpression[] = []; + const tableExpressions: lua.Expression[] = []; + for (const property of properties) { + if (lua.isTableFieldExpression(property)) { + fields.push(property); + } else { + if (fields.length > 0) { + tableExpressions.push(lua.createTableExpression(fields)); + } + tableExpressions.push(property); + fields = []; + } } if (tableExpressions.length === 0) { - return lua.createTableExpression(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); } if (tableExpressions[0].kind !== lua.SyntaxKind.TableExpression) { tableExpressions.unshift(lua.createTableExpression(undefined, expression)); } - return transformLuaLibFunction(context, LuaLibFeature.ObjectAssign, expression, ...tableExpressions); } }; @@ -139,7 +199,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); }; diff --git a/src/transformation/visitors/loops/do-while.ts b/src/transformation/visitors/loops/do-while.ts index 0fafc710a..77f172007 100644 --- a/src/transformation/visitors/loops/do-while.ts +++ b/src/transformation/visitors/loops/do-while.ts @@ -1,23 +1,68 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { FunctionVisitor } from "../../context"; -import { transformLoopBody } from "./utils"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; +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); + + let [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () => + context.transformExpression(statement.expression) ); + // 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()]), + undefined, + statement.expression + ) + ); + body.unshift(...conditionPrecedingStatements); + 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); + + let [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () => + invertCondition(context.transformExpression(statement.expression)) + ); + + // 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()]), + undefined, + statement.expression + ) + ); + condition = lua.createBooleanLiteral(false); } - return lua.createRepeatStatement(lua.createBlock([body]), 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 98864082e..3fbbb6353 100644 --- a/src/transformation/visitors/loops/for.ts +++ b/src/transformation/visitors/loops/for.ts @@ -1,8 +1,9 @@ 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 { transformLoopBody } from "./utils"; +import { invertCondition, transformLoopBody } from "./utils"; export const transformForStatement: FunctionVisitor = (statement, context) => { const result: lua.Statement[] = []; @@ -17,19 +18,47 @@ 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) { + let conditionPrecedingStatements: lua.Statement[]; + const tsCondition = statement.condition; + [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () => + context.transformExpression(tsCondition) + ); + + // 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()]), + undefined, + statement.condition + ) + ); + body.unshift(...conditionPrecedingStatements); + condition = lua.createBooleanLiteral(true); + } + } else { + condition = lua.createBooleanLiteral(true); + } + if (statement.incrementor) { body.push(...context.transformStatements(ts.factory.createExpressionStatement(statement.incrementor))); } // 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 a7402b653..ec3548baa 100644 --- a/src/transformation/visitors/loops/utils.ts +++ b/src/transformation/visitors/loops/utils.ts @@ -64,10 +64,20 @@ export function transformForInitializer( block.statements.unshift( ...(isAssignmentPattern(initializer) - ? transformAssignmentPattern(context, initializer, valueVariable) + ? transformAssignmentPattern(context, initializer, valueVariable, false) : transformAssignment(context, initializer, valueVariable)) ); } return valueVariable; } + +export function invertCondition(expression: lua.Expression) { + if (lua.isUnaryExpression(expression) && expression.operator === lua.SyntaxKind.NotOperator) { + return expression.operand; + } else { + const notExpression = lua.createUnaryExpression(expression, lua.SyntaxKind.NotOperator); + lua.setNodePosition(notExpression, lua.getOriginalPos(expression)); + return notExpression; + } +} diff --git a/src/transformation/visitors/sourceFile.ts b/src/transformation/visitors/sourceFile.ts index 41561c67f..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,7 +14,11 @@ export const transformSourceFileNode: FunctionVisitor = (node, co const [statement] = node.statements; if (statement) { assert(ts.isExpressionStatement(statement)); - statements.push(lua.createReturnStatement([context.transformExpression(statement.expression)])); + 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"), [ lua.createStringLiteral("Unexpected end of JSON input"), diff --git a/src/transformation/visitors/switch.ts b/src/transformation/visitors/switch.ts index ae01ff10d..e14df3002 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 { createShortCircuitBinaryExpressionPrecedingStatements } from "./binary-expression"; const containsBreakOrReturn = (nodes: Iterable): boolean => { for (const s of nodes) { @@ -17,31 +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 createShortCircuitBinaryExpressionPrecedingStatements( + context, + left, + right, + rightPrecedingStatements, + ts.SyntaxKind.BarBarToken + ); + } else { + return [rightPrecedingStatements, lua.createBinaryExpression(left, right, lua.SyntaxKind.OrOperator)]; + } +}; + const coalesceCondition = ( condition: lua.Expression | undefined, + conditionPrecedingStatements: lua.Statement[], switchVariable: lua.Identifier, expression: ts.Expression, context: TransformationContext -): lua.Expression => { +): [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 lua.createBinaryExpression( - condition, - lua.createBinaryExpression( - switchVariable, - context.transformExpression(expression), - lua.SyntaxKind.EqualityOperator - ), - lua.SyntaxKind.OrOperator - ); + return createOrExpression(context, condition, comparison, precedingStatements); } // Next condition - return lua.createBinaryExpression( - switchVariable, - context.transformExpression(expression), - lua.SyntaxKind.EqualityOperator - ); + return [[...conditionPrecedingStatements, ...precedingStatements], comparison]; }; export const transformSwitchStatement: FunctionVisitor = (statement, context) => { @@ -76,6 +95,7 @@ export const transformSwitchStatement: FunctionVisitor = (st let defaultTransformed = false; let isInitialCondition = true; 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]; @@ -88,25 +108,41 @@ export const transformSwitchStatement: FunctionVisitor = (st // Compute the condition for the if statement if (!ts.isDefaultClause(clause)) { - condition = coalesceCondition(condition, switchVariable, clause.expression, context); + [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( - isInitialCondition - ? lua.createVariableDeclarationStatement(conditionVariable, condition) - : lua.createAssignmentStatement( - conditionVariable, - lua.createBinaryExpression(conditionVariable, condition, lua.SyntaxKind.OrOperator) - ) - ); + 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) { statements.push( + ...conditionPrecedingStatements, lua.createVariableDeclarationStatement( conditionVariable, condition ?? lua.createBooleanLiteral(false) @@ -115,6 +151,7 @@ export const transformSwitchStatement: FunctionVisitor = (st // Clear condition ot ensure it is not evaluated twice condition = undefined; + conditionPrecedingStatements = []; isInitialCondition = false; } @@ -122,11 +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( - lua.createAssignmentStatement( - conditionVariable, - lua.createBinaryExpression(conditionVariable, condition, lua.SyntaxKind.OrOperator) - ) + ...conditionPrecedingStatements, + lua.createAssignmentStatement(conditionVariable, condition) ); } continue; @@ -155,6 +196,7 @@ export const transformSwitchStatement: FunctionVisitor = (st // Clear condition for next clause condition = undefined; + conditionPrecedingStatements = []; } // If no conditions above match, we need to create the final default case code-path, 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/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/src/transformation/visitors/variable-declaration.ts b/src/transformation/visitors/variable-declaration.ts index 4782fd157..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,7 +64,11 @@ export function transformBindingPattern( // The identifier of the new variable const variableName = transformIdentifier(context, element.name); // The field to extract - const propertyName = transformPropertyName(context, element.propertyName ?? element.name); + 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) { @@ -123,11 +128,16 @@ export function transformBindingPattern( result.push(...createLocalOrExportedOrGlobalDeclaration(context, variableName, expression)); if (element.initializer) { const identifier = addExportToIdentifier(context, variableName); + const tsInitializer = element.initializer; + const [initializerPrecedingStatements, initializer] = transformInPrecedingStatementScope(context, () => + context.transformExpression(tsInitializer) + ); 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 +165,15 @@ export function transformBindingVariableDeclaration( table = transformIdentifier(context, initializer); } else { // Contain the expression in a temporary variable - table = lua.createAnonymousIdentifier(); if (initializer) { + table = context.createTempNameForNode(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/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/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index fb4594742..9671f656c 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/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index cfd0e53cb..91bc2306b 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 f9911b514..6bd3922eb 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -61,7 +61,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 ____ = {{____(_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."`; @@ -166,8 +166,8 @@ local function multi(self, ...) return ... end local a -local ____ = {____(nil)} -a = ____[1] +local ____temp_0 = {____(nil)} +a = ____temp_0[1] ____exports.a = a ____exports.a = a return ____exports" @@ -182,8 +182,8 @@ local function multi(self, ...) end local a do - local ____ = {____(nil, 1, 2)} - a = ____[1] + local ____temp_0 = {____(nil, 1, 2)} + a = ____temp_0[1] ____exports.a = a while false do local ____ = 1 @@ -219,12 +219,10 @@ local function multi(self, ...) return ... end local a -if (function() - local ____ = {____(nil, 1)} - a = ____[1] - ____exports.a = a - return ____ -end)() then +local _____24multi_result_0 = {____(nil, 1)} +a = _____24multi_result_0[1] +____exports.a = a +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 7a3387f5e..211b69e6b 100644 --- a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap @@ -47,79 +47,61 @@ 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)() -)" +"local ____foo_1 = foo +local ____G_0 = _G; +({}).foo = 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."`; 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)() -)" +"local ____foo_1 = foo +local ____G_0 = _G; +({}).foo = 3 +____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 new file mode 100644 index 000000000..6ff6b4706 --- /dev/null +++ b/test/unit/precedingStatements.spec.ts @@ -0,0 +1,625 @@ +import * as util from "../util"; + +const shortCircuitTests: Array<{ operator: string; testValue: unknown }> = [ + { 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 }, +]; + +test.each(shortCircuitTests)("short circuit operator (%p)", ({ operator, testValue }) => { + util.testFunction` + let x: unknown = ${testValue}; + let y = 1; + const z = x ${operator} y++; + return {x, y, z}; + `.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; + let condition: boolean = ${condition}; + const c = condition ? a++ : b++; + return [a, b, c]; + `.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", + "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 => { + util.testFunction` + const a = [7, 8, 9]; + let i = 0; + function inc() { ++i; return i; } + return [${sequence}]; + `.expectToMatchJsResult(); + }); + + test.each(sequenceTests)("function arguments (foo(%p))", sequence => { + 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(); + }); + + 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(); + }); + + 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; + const d = (a++, b += a, c += b); + return [a, b, c, d]; + `.expectToMatchJsResult(); + }); + + test("template expression", () => { + util.testFunction` + let i = 0; + return \`\${i}, \${i++}, \${i}\`; + `.expectToMatchJsResult(); + }); + + test("tagged template literal", () => { + util.testFunction` + let i = 0; + + function func(strings: TemplateStringsArray, ...expressions: any[]) { + const x = i > 0 ? "a" : "b"; + return { strings: [x, ...strings], raw: strings.raw, expressions }; + } + + return func\`hello \${i} \${i++}\`; + `.expectToMatchJsResult(); + }); + + test("binary operators", () => { + util.testFunction` + let i = 0; + return i + i++; + `.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("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", () => { + test("index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [4, 5]; + a[i] = i++; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const result = a[i] = i++; + return [result, a, i]; + `.expectToMatchJsResult(); + }); + + test("indirect index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { return (x > 0) ? b : a; } + foo(i)[i] = i++; + return [a, b, i]; + `.expectToMatchJsResult(); + }); + + test("indirect index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + 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 statement", () => { + util.testFunction` + const a = {value: 10}; + const b = {value: 11}; + let i = 0; + 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; + const a = [9, 8]; + a[i] += i++; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("compound index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const result = a[i] += i++; + return [result, a, i]; + `.expectToMatchJsResult(); + }); + + test("compound indirect index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { return (x > 0) ? b : a; } + foo(i)[i] += i++; + return [a, b, i]; + `.expectToMatchJsResult(); + }); + + test("compound indirect index assignment expression", () => { + util.testFunction` + let i = 1; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { return (x > 0) ? b : a; } + const result = foo(i)[i] += i++; + return [result, a, b, i]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment statement", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + [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 result = [a[i], a[i++]] = [i++, i++]; + return [a, i, result]; + `.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("array destructuring assignment expression with default", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + const result = [a[i] = i++, a[i++]] = [i++, i++]; + return [a, i, result]; + `.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 result = [a[0][i], ...a[i++]] = [i++, i++]; + return [a, i, result]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment statement", () => { + util.testFunction` + let s = "A"; + const o: Record = {ABCDEFG: "success", result: ""}; + 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 s = "A"; + const o: Record = {ABCDEFGHIJ: "success", result: ""}; + 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 s = "A"; + const o: Record = {ABCDEFG: "success", result: ""}; + 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 s = "A"; + const o: Record = {ABCDEFGHIJ: "success", result: ""}; + 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 s = "A"; + const o: Record = {ABCDE: "success"}; + 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 s = "A"; + const o: Record = {ABCDEFGH: "success"}; + 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(); + }); + + test("call expression", () => { + util.testFunction` + let i = 1; + function a(x: number) { return x * 10; } + function b(x: number) { return x * 100; } + function foo(x: number) { return (x > 0) ? b : a; } + const result = foo(i)(i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("call expression (function modified)", () => { + util.testFunction` + let i = 1; + let foo = (x: null, y: number) => { return y; }; + function changeFoo() { + foo = (x: null, y: number) => { return y * 10; }; + return null; + } + const result = foo(changeFoo(), 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" as const; } + 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" as const; } + 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) { return (x > 0) ? b : 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) { return (x > 0) ? b : 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) { return (x > 0) ? "foo" : "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) { return (x > 0) ? B : A; } + let i = 0; + const result = new (foo(i))(i++).val; + return [result, i]; + `.expectToMatchJsResult(); + }); +}); + +describe("loop expressions", () => { + test("while loop", () => { + util.testFunction` + let i = 0, j = 0; + while (i++ < 5) { + ++j; + if (j >= 10) { + break; + } + } + return [i, j]; + `.expectToMatchJsResult(); + }); + + test("for loop", () => { + util.testFunction` + let j = 0; + for (let i = 0; i++ < 5;) { + ++j; + if (j >= 10) { + break; + } + } + return j; + `.expectToMatchJsResult(); + }); + + test("do while loop", () => { + util.testFunction` + let i = 0, j = 0; + do { + ++j; + if (j >= 10) { + break; + } + } while (i++ < 5); + return [i, j]; + `.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(); + }); +}); + +test("switch", () => { + util.testFunction` + let i = 0; + let x = 0; + let result = ""; + switch (x) { + case i++: + result = "test"; + break; + case i++: + } + return [i, result]; + `.expectToMatchJsResult(); +}); + +test("else if", () => { + util.testFunction` + let i = 0; + if (i++ === 0) { + } else if (i++ === 1) { + } + return i; + `.expectToMatchJsResult(); +});