diff --git a/src/CommandLineParser.ts b/src/CommandLineParser.ts index 989abeb14..cf1a349e2 100644 --- a/src/CommandLineParser.ts +++ b/src/CommandLineParser.ts @@ -32,6 +32,11 @@ export const optionDeclarations: YargsOptions = { describe: "Specify if a header will be added to compiled files.", type: "boolean", }, + noHoisting: { + default: false, + describe: "Disables hoisting.", + type: "boolean", + }, }; class CLIError extends Error {} diff --git a/src/Compiler.ts b/src/Compiler.ts index 112bce125..9dfeadf30 100644 --- a/src/Compiler.ts +++ b/src/Compiler.ts @@ -119,4 +119,4 @@ export function transpileString( const result = transpiler.transpileSourceFile(program.getSourceFile("file.ts")); return result.trim(); -} \ No newline at end of file +} diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index e6e2a2aa1..24fae4374 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -4,6 +4,7 @@ export interface CompilerOptions extends ts.CompilerOptions { noHeader?: boolean; luaTarget?: LuaTarget; luaLibImport?: LuaLibImportKind; + noHoisting?: boolean; } export enum LuaLibImportKind { diff --git a/src/LuaAST.ts b/src/LuaAST.ts index 31ab9975d..424d09057 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -97,6 +97,8 @@ export type BinaryOperator = export type Operator = UnaryOperator | BinaryOperator; +export type SymbolId = number; + // TODO For future sourcemap support? export interface TextRange { pos: number; @@ -791,15 +793,23 @@ export function createMethodCallExpression( export interface Identifier extends Expression { kind: SyntaxKind.Identifier; text: string; + symbolId?: SymbolId; } export function isIdentifier(node: Node): node is Identifier { return node.kind === SyntaxKind.Identifier; } -export function createIdentifier(text: string | ts.__String, tsOriginal?: ts.Node, parent?: Node): Identifier { +export function createIdentifier( + text: string | ts.__String, + tsOriginal?: ts.Node, + symbolId?: SymbolId, + parent?: Node +): Identifier +{ const expression = createNode(SyntaxKind.Identifier, tsOriginal, parent) as Identifier; expression.text = text as string; + expression.symbolId = symbolId; return expression; } diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index c15e28934..038cea273 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -12,14 +12,30 @@ export type StatementVisitResult = tstl.Statement | tstl.Statement[] | undefined export type ExpressionVisitResult = tstl.Expression | undefined; export enum ScopeType { - Function, - Switch, - Loop, + File = 0x1, + Function = 0x2, + Switch = 0x4, + Loop = 0x8, + Conditional = 0x10, + Block = 0x20, +} + +interface SymbolInfo { + symbol: ts.Symbol; + firstSeenAtPos: number; +} + +interface FunctionDefinitionInfo { + referencedSymbols: Set; + assignment?: tstl.AssignmentStatement; } interface Scope { type: ScopeType; id: number; + referencedSymbols?: Set; + variableDeclarations?: tstl.VariableDeclarationStatement[]; + functionDefinitions?: Map; } export class LuaTransformer { @@ -44,6 +60,10 @@ export class LuaTransformer { private luaLibFeatureSet: Set; + private symbolInfo: Map; + private symbolIds: Map; + private genSymbolIdCounter: number; + private readonly typeValidationCache: Map> = new Map>(); public constructor(program: ts.Program, options: CompilerOptions) { @@ -60,23 +80,28 @@ export class LuaTransformer { } public setupState(): void { - this.scopeStack = []; this.genVarCounter = 0; this.currentSourceFile = undefined; this.isModule = false; this.scopeStack = []; this.classStack = []; this.luaLibFeatureSet = new Set(); + this.symbolIds = new Map(); + this.symbolInfo = new Map(); + this.genSymbolIdCounter = 1; } // TODO make all other methods private??? public transformSourceFile(node: ts.SourceFile): [tstl.Block, Set] { this.setupState(); + this.pushScope(ScopeType.File, node); this.currentSourceFile = node; this.isModule = tsHelper.isFileModule(node); - const statements = this.transformStatements(node.statements); + const statements = this.performHoisting(this.transformStatements(node.statements)); + this.popScope(); + if (this.isModule) { statements.unshift( tstl.createVariableDeclarationStatement( @@ -104,7 +129,7 @@ export class LuaTransformer { switch (node.kind) { // Block case ts.SyntaxKind.Block: - return this.transformScopeBlock(node as ts.Block); + return this.transformBlockAsDoStatement(node as ts.Block); // Declaration Statements case ts.SyntaxKind.ImportDeclaration: return this.transformImportDeclaration(node as ts.ImportDeclaration); @@ -168,11 +193,17 @@ export class LuaTransformer { } public transformBlock(block: ts.Block): tstl.Block { - return tstl.createBlock(this.transformStatements(block.statements), block); + this.pushScope(ScopeType.Block, block); + const statements = this.performHoisting(this.transformStatements(block.statements)); + this.popScope(); + return tstl.createBlock(statements, block); } - public transformScopeBlock(block: ts.Block): tstl.DoStatement { - return tstl.createDoStatement(this.transformStatements(block.statements), block); + public transformBlockAsDoStatement(block: ts.Block): tstl.DoStatement { + this.pushScope(ScopeType.Block, block); + const statements = this.performHoisting(this.transformStatements(block.statements)); + this.popScope(); + return tstl.createDoStatement(statements, block); } public transformImportDeclaration(statement: ts.ImportDeclaration): StatementVisitResult { @@ -214,16 +245,15 @@ export class LuaTransformer { if (importSpecifier.propertyName) { const propertyIdentifier = this.transformIdentifier(importSpecifier.propertyName); const propertyName = tstl.createStringLiteral(propertyIdentifier.text); - const renamedImport = tstl.createVariableDeclarationStatement( - this.transformIdentifier(importSpecifier.name), + const renamedImport = this.createHoistableVariableDeclarationStatement( + importSpecifier.name, tstl.createTableIndexExpression(importUniqueName, propertyName), importSpecifier); result.push(renamedImport); } else { - const nameIdentifier = this.transformIdentifier(importSpecifier.name); - const name = tstl.createStringLiteral(nameIdentifier.text); - const namedImport = tstl.createVariableDeclarationStatement( - nameIdentifier, + const name = tstl.createStringLiteral(importSpecifier.name.text); + const namedImport = this.createHoistableVariableDeclarationStatement( + importSpecifier.name, tstl.createTableIndexExpression(importUniqueName, name), importSpecifier ); @@ -232,8 +262,8 @@ export class LuaTransformer { }); return result; } else if (ts.isNamespaceImport(imports)) { - const requireStatement = tstl.createVariableDeclarationStatement( - this.transformIdentifier(imports.name), + const requireStatement = this.createHoistableVariableDeclarationStatement( + imports.name, requireCall, statement ); @@ -588,16 +618,16 @@ export class LuaTransformer { this.createSelfIdentifier() ); - bodyStatements.push(...this.transformFunctionBody(statement.parameters, statement.body, restParamName)); - - const body: tstl.Block = tstl.createBlock(bodyStatements); + const [body] = this.transformFunctionBody(statement.parameters, statement.body, restParamName); + bodyStatements.push(...body); + const block: tstl.Block = tstl.createBlock(bodyStatements); const result = tstl.createAssignmentStatement( tstl.createTableIndexExpression( this.addExportToIdentifier(className), tstl.createStringLiteral("constructor")), - tstl.createFunctionExpression(body, params, dotsLiteral, restParamName, undefined, undefined), + tstl.createFunctionExpression(block, params, dotsLiteral, restParamName, undefined, undefined), statement); this.classStack.pop(); @@ -613,8 +643,9 @@ export class LuaTransformer { { const name = this.transformIdentifier(getAccessor.name as ts.Identifier); + const [body] = this.transformFunctionBody(getAccessor.parameters, getAccessor.body); const accessorFunction = tstl.createFunctionExpression( - tstl.createBlock(this.transformFunctionBody(getAccessor.parameters, getAccessor.body)), + tstl.createBlock(body), [this.createSelfIdentifier()] ); @@ -636,8 +667,9 @@ export class LuaTransformer { const [params, dot, restParam] = this.transformParameters(setAccessor.parameters, this.createSelfIdentifier()); + const [body] = this.transformFunctionBody(setAccessor.parameters, setAccessor.body, restParam); const accessorFunction = tstl.createFunctionExpression( - tstl.createBlock(this.transformFunctionBody(setAccessor.parameters, setAccessor.body, restParam)), + tstl.createBlock(body), params, dot, restParam @@ -672,8 +704,9 @@ export class LuaTransformer { : undefined; const [paramNames, dots, restParamName] = this.transformParameters(node.parameters, context); + const [body] = this.transformFunctionBody(node.parameters, node.body, restParamName); const functionExpression = tstl.createFunctionExpression( - tstl.createBlock(this.transformFunctionBody(node.parameters, node.body, restParamName)), + tstl.createBlock(body), paramNames, dots, restParamName @@ -730,9 +763,9 @@ export class LuaTransformer { parameters: ts.NodeArray, body: ts.Block, spreadIdentifier?: tstl.Identifier - ): tstl.Statement[] + ): [tstl.Statement[], Scope] { - this.pushScope(ScopeType.Function); + this.pushScope(ScopeType.Function, body); const headerStatements = []; @@ -761,11 +794,11 @@ export class LuaTransformer { headerStatements.push(tstl.createVariableDeclarationStatement(spreadIdentifier, spreadTable)); } - const bodyStatements = this.transformStatements(body.statements); + const bodyStatements = this.performHoisting(this.transformStatements(body.statements)); - this.popScope(); + const scope = this.popScope(); - return headerStatements.concat(bodyStatements); + return [headerStatements.concat(bodyStatements), scope]; } public transformParameterDefaultValueDeclaration(declaration: ts.ParameterDeclaration): tstl.Statement { @@ -860,13 +893,14 @@ export class LuaTransformer { result.push(namespaceDeclaration); // local innerNS = outerNS.innerNS - const localDeclaration = tstl.createVariableDeclarationStatement( - this.transformIdentifier(statement.name as ts.Identifier), + const localDeclaration = this.createHoistableVariableDeclarationStatement( + statement.name as ts.Identifier, tstl.createTableIndexExpression( this.transformIdentifier(this.currentNamespace.name as ts.Identifier), tstl.createStringLiteral(this.transformIdentifier(statement.name as ts.Identifier).text))); result.push(localDeclaration); + } else if (this.isModule && (ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export)) { // exports.NS = exports.NS or {} const namespaceDeclaration = tstl.createAssignmentStatement( @@ -879,11 +913,12 @@ export class LuaTransformer { result.push(namespaceDeclaration); // local NS = exports.NS - const localDeclaration = tstl.createVariableDeclarationStatement( - this.transformIdentifier(statement.name as ts.Identifier), + const localDeclaration = this.createHoistableVariableDeclarationStatement( + statement.name as ts.Identifier, this.createExportedIdentifier(this.transformIdentifier(statement.name as ts.Identifier))); result.push(localDeclaration); + } else { // local NS = NS or {} const localDeclaration = this.createLocalOrExportedOrGlobalDeclaration( @@ -905,7 +940,10 @@ export class LuaTransformer { // Transform moduleblock to block and visit it if (statement.body && ts.isModuleBlock(statement.body)) { - result.push(tstl.createDoStatement(this.transformStatements(statement.body.statements))); + this.pushScope(ScopeType.Block, statement); + const statements = this.performHoisting(this.transformStatements(statement.body.statements)); + this.popScope(); + result.push(tstl.createDoStatement(statements)); } this.currentNamespace = previousNamespace; @@ -1006,10 +1044,21 @@ export class LuaTransformer { const [params, dotsLiteral, restParamName] = this.transformParameters(functionDeclaration.parameters, context); const name = this.transformIdentifier(functionDeclaration.name); - const body = tstl.createBlock( - this.transformFunctionBody(functionDeclaration.parameters, functionDeclaration.body, restParamName) + const [body, functionScope] = this.transformFunctionBody( + functionDeclaration.parameters, + functionDeclaration.body, + restParamName ); - const functionExpression = tstl.createFunctionExpression(body, params, dotsLiteral, restParamName); + const block = tstl.createBlock(body); + const functionExpression = tstl.createFunctionExpression(block, params, dotsLiteral, restParamName); + + // Remember symbols referenced in this function for hoisting later + if (!this.options.noHoisting && name.symbolId !== undefined) { + const scope = this.peekScope(); + if (!scope.functionDefinitions) { scope.functionDefinitions = new Map(); } + const functionInfo = {referencedSymbols: functionScope.referencedSymbols || new Set()}; + scope.functionDefinitions.set(name.symbolId, functionInfo); + } return this.createLocalOrExportedOrGlobalDeclaration(name, functionExpression, functionDeclaration); } @@ -1041,7 +1090,7 @@ export class LuaTransformer { } else { return this.createLocalOrExportedOrGlobalDeclaration( identifierName, - tstl.createNilLiteral(), + undefined, statement ); } @@ -1200,13 +1249,19 @@ export class LuaTransformer { } public transformIfStatement(statement: ts.IfStatement): tstl.IfStatement { + this.pushScope(ScopeType.Conditional, statement.thenStatement); const condition = this.transformExpression(statement.expression); - const ifBlock = tstl.createBlock(this.transformBlockOrStatement(statement.thenStatement)); + const statements = this.performHoisting(this.transformBlockOrStatement(statement.thenStatement)); + this.popScope(); + const ifBlock = tstl.createBlock(statements); if (statement.elseStatement) { if (ts.isIfStatement(statement.elseStatement)) { return tstl.createIfStatement(condition, ifBlock, this.transformIfStatement(statement.elseStatement)); } else { - const elseBlock = tstl.createBlock(this.transformBlockOrStatement(statement.elseStatement)); + this.pushScope(ScopeType.Conditional, statement.elseStatement); + const elseStatements = this.performHoisting(this.transformBlockOrStatement(statement.elseStatement)); + this.popScope(); + const elseBlock = tstl.createBlock(elseStatements); return tstl.createIfStatement(condition, ifBlock, elseBlock); } } @@ -1289,9 +1344,10 @@ export class LuaTransformer { loop: ts.WhileStatement | ts.DoStatement | ts.ForStatement | ts.ForOfStatement | ts.ForInOrOfStatement ): tstl.Statement[] { - this.pushScope(ScopeType.Loop); - const body = this.transformBlockOrStatement(loop.statement); - const scopeId = this.popScope().id; + this.pushScope(ScopeType.Loop, loop.statement); + const body = this.performHoisting(this.transformBlockOrStatement(loop.statement)); + const scope = this.popScope(); + const scopeId = scope.id; if (this.options.luaTarget === LuaTarget.Lua51) { return body; @@ -1492,16 +1548,16 @@ export class LuaTransformer { throw TSTLErrors.UnsupportedForTarget("Switch statements", this.options.luaTarget, statement); } - this.pushScope(ScopeType.Switch); + this.pushScope(ScopeType.Switch, statement); // Give the switch a unique name to prevent nested switches from acting up. - const switchName = `____TS_switch${this.scopeStack.length}`; + const switchName = `____TS_switch${this.peekScope().id}`; const expression = this.transformExpression(statement.expression); const switchVariable = tstl.createIdentifier(switchName); const switchVariableDeclaration = tstl.createVariableDeclarationStatement(switchVariable, expression); - const statements: tstl.Statement[] = [switchVariableDeclaration]; + let statements: tstl.Statement[] = [switchVariableDeclaration]; const caseClauses = statement.caseBlock.clauses.filter(c => ts.isCaseClause(c)) as ts.CaseClause[]; @@ -1537,14 +1593,16 @@ export class LuaTransformer { statements.push(tstl.createLabelStatement(`${switchName}_end`)); + statements = this.performHoisting(statements); this.popScope(); return statements; } public transformBreakStatement(breakStatement: ts.BreakStatement): StatementVisitResult { - if (this.peekScope().type === ScopeType.Switch) { - return tstl.createGotoStatement(`____TS_switch${this.scopeStack.length}_end`); + const breakableScope = this.findScope(ScopeType.Loop | ScopeType.Switch); + if (breakableScope.type === ScopeType.Switch) { + return tstl.createGotoStatement(`____TS_switch${breakableScope.id}_end`); } else { return tstl.createBreakStatement(breakStatement); } @@ -1604,7 +1662,7 @@ export class LuaTransformer { } return tstl.createGotoStatement( - `__continue${this.peekScope().id}`, + `__continue${this.findScope(ScopeType.Loop).id}`, statement ); } @@ -2356,7 +2414,7 @@ export class LuaTransformer { ); const body = ts.isBlock(node.body) ? node.body : ts.createBlock([ts.createReturn(node.body)]); - const transformedBody = this.transformFunctionBody(node.parameters, body, spreadIdentifier); + const [transformedBody] = this.transformFunctionBody(node.parameters, body, spreadIdentifier); return tstl.createFunctionExpression( tstl.createBlock(transformedBody), @@ -3095,6 +3153,7 @@ export class LuaTransformer { // But this should be changed to retun tstl.createNilLiteral() // at some point. } + let escapedText = expression.escapedText as string; const underScoreCharCode = "_".charCodeAt(0); if (escapedText.length >= 3 && escapedText.charCodeAt(0) === underScoreCharCode && @@ -3105,7 +3164,9 @@ export class LuaTransformer { if (this.luaKeywords.has(escapedText)) { throw TSTLErrors.KeywordIdentifier(expression); } - return tstl.createIdentifier(escapedText, expression); + + const symbolId = this.getIdentifierSymbolId(expression); + return tstl.createIdentifier(escapedText, expression, symbolId); } public transformIdentifierExpression(expression: ts.Identifier): tstl.IdentifierOrTableIndexExpression { @@ -3243,44 +3304,90 @@ export class LuaTransformer { private createLocalOrExportedOrGlobalDeclaration( lhs: tstl.Identifier | tstl.Identifier[], - rhs: tstl.Expression, + rhs?: tstl.Expression, tsOriginal?: ts.Node, parent?: tstl.Node ): tstl.Statement[] { + let declaration: tstl.VariableDeclarationStatement | undefined; + let assignment: tstl.AssignmentStatement | undefined; + + const functionDeclaration = tsOriginal && ts.isFunctionDeclaration(tsOriginal) ? tsOriginal : undefined; + if (this.shouldExportIdentifier(lhs)) { // exported - if (Array.isArray(lhs)) { - return [tstl.createAssignmentStatement( - lhs.map(i => this.createExportedIdentifier(i)), rhs, undefined, parent - )]; + if (!rhs) { + return []; + + } else if (Array.isArray(lhs)) { + assignment = tstl.createAssignmentStatement( + lhs.map(i => this.createExportedIdentifier(i)), + rhs, + tsOriginal, + parent + ); + } else { - return [tstl.createAssignmentStatement(this.createExportedIdentifier(lhs), rhs, undefined, parent)]; + assignment = tstl.createAssignmentStatement( + this.createExportedIdentifier(lhs), + rhs, + tsOriginal, + parent + ); } - } - const insideFunction = this.scopeStack.some(s => s.type === ScopeType.Function); - const isLetOrConst = tsOriginal && ts.isVariableDeclaration(tsOriginal) - && tsOriginal.parent && (tsOriginal.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; - if (this.isModule || this.currentNamespace || insideFunction || isLetOrConst) { - // local - const isFunction = - tsOriginal - && (ts.isFunctionDeclaration(tsOriginal) - || (ts.isVariableDeclaration(tsOriginal) && ts.isFunctionLike(tsOriginal.initializer))); - if (isFunction) { - // Separate declaration from assignment for functions to allow recursion - return [ - tstl.createVariableDeclarationStatement(lhs, undefined, tsOriginal, parent), - tstl.createAssignmentStatement(lhs, rhs, tsOriginal, parent), - ]; + } else { + const insideFunction = this.findScope(ScopeType.Function) !== undefined; + let isLetOrConst = false; + let isFirstDeclaration = true; // var can have multiple declarations for the same variable :/ + if (tsOriginal && ts.isVariableDeclaration(tsOriginal)) { + isLetOrConst = tsOriginal.parent + && (tsOriginal.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; + isFirstDeclaration = isLetOrConst || tsHelper.isFirstDeclaration(tsOriginal, this.checker); + } + if ((this.isModule || this.currentNamespace || insideFunction || isLetOrConst) && isFirstDeclaration) { + // local + if (rhs && tstl.isFunctionExpression(rhs)) { + declaration = tstl.createVariableDeclarationStatement(lhs, undefined, tsOriginal, parent); + assignment = tstl.createAssignmentStatement(lhs, rhs, tsOriginal, parent); + + } else { + declaration = tstl.createVariableDeclarationStatement(lhs, rhs, tsOriginal, parent); + } + + if (!this.options.noHoisting) { + // Remember local variable declarations for hoisting later + const scope = isLetOrConst || functionDeclaration + ? this.peekScope() + : this.findScope(ScopeType.Function | ScopeType.File); + + if (!scope.variableDeclarations) { scope.variableDeclarations = []; } + scope.variableDeclarations.push(declaration); + } + + } else if (rhs) { + // global + assignment = tstl.createAssignmentStatement(lhs, rhs, tsOriginal, parent); + } else { - return [tstl.createVariableDeclarationStatement(lhs, rhs, tsOriginal, parent)]; + return []; + } + } + + if (!this.options.noHoisting && functionDeclaration) { + // Remember function definitions for hoisting later + const functionSymbolId = (lhs as tstl.Identifier).symbolId; + if (functionSymbolId !== undefined) { + this.peekScope().functionDefinitions.get(functionSymbolId).assignment = assignment; } + } + if (declaration && assignment) { + return [declaration, assignment]; + } else if (declaration) { + return [declaration]; } else { - // global - return [tstl.createAssignmentStatement(lhs, rhs, tsOriginal, parent)]; + return [assignment]; } } @@ -3370,17 +3477,191 @@ export class LuaTransformer { return tstl.createBinaryExpression(expression, tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator); } + private getIdentifierSymbolId(identifier: ts.Identifier): tstl.SymbolId { + const symbol = this.checker.getSymbolAtLocation(identifier); + let symbolId: number | undefined; + if (symbol) { + if (this.options.noHoisting) { + // Check for reference-before-declaration + const declaration = tsHelper.getFirstDeclaration(symbol, this.currentSourceFile); + if (declaration && identifier.pos < declaration.pos) { + throw TSTLErrors.ReferencedBeforeDeclaration(identifier); + } + + } else { + // Track first time symbols are seen + if (!this.symbolIds.has(symbol)) { + symbolId = this.genSymbolIdCounter++; + const symbolInfo: SymbolInfo = {symbol, firstSeenAtPos: identifier.pos}; + this.symbolIds.set(symbol, symbolId); + this.symbolInfo.set(symbolId, symbolInfo); + } else { + symbolId = this.symbolIds.get(symbol); + } + + //Mark symbol as seen in all current scopes + this.scopeStack.forEach(s => { + if (!s.referencedSymbols) { s.referencedSymbols = new Set(); } + s.referencedSymbols.add(symbolId); + }); + } + } + return symbolId; + } + + protected findScope(scopeTypes: ScopeType): Scope | undefined { + return this.scopeStack.slice().reverse().find(s => (scopeTypes & s.type) !== 0); + } + protected peekScope(): Scope { return this.scopeStack[this.scopeStack.length - 1]; } - protected pushScope(scopeType: ScopeType): void { - this.scopeStack.push({type: scopeType, id: this.genVarCounter}); + protected pushScope(scopeType: ScopeType, node: ts.Node): void { + this.scopeStack.push({ + type: scopeType, + id: this.genVarCounter, + }); this.genVarCounter++; } + private shouldHoist(symbolId: tstl.SymbolId, scope: Scope): boolean { + const symbolInfo = this.symbolInfo.get(symbolId); + if (!symbolInfo) { + return false; + } + + const declaration = tsHelper.getFirstDeclaration(symbolInfo.symbol, this.currentSourceFile); + if (!declaration) { + return false; + } + + if (symbolInfo.firstSeenAtPos < declaration.pos) { + return true; + } + + if (scope.functionDefinitions) { + for (const [functionSymbolId, functionDefinition] of scope.functionDefinitions) { + if (functionSymbolId !== symbolId // Don't recurse into self + && declaration.pos < functionDefinition.assignment.pos // Ignore functions before symbol declaration + && functionDefinition.referencedSymbols.has(symbolId) + && this.shouldHoist(functionSymbolId, scope)) + { + return true; + } + } + } + + return false; + } + + protected replaceStatementInParent(oldNode: tstl.Statement, newNode?: tstl.Statement): void { + if (!oldNode.parent) { + throw new Error("node has not yet been assigned a parent"); + } + + if (tstl.isBlock(oldNode.parent) || tstl.isDoStatement(oldNode.parent)) { + if (newNode) { + oldNode.parent.statements.splice(oldNode.parent.statements.indexOf(oldNode), 1, newNode); + } else { + oldNode.parent.statements.splice(oldNode.parent.statements.indexOf(oldNode), 1); + } + } else { + throw new Error("unexpected parent type"); + } + } + + protected hoistFunctionDefinitions(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] { + if (!scope.functionDefinitions) { + return statements; + } + + const result = statements.slice(); + const hoistedFunctions: tstl.AssignmentStatement[] = []; + for (const [functionSymbolId, functionDefinition] of scope.functionDefinitions) { + if (this.shouldHoist(functionSymbolId, scope)) { + const i = result.indexOf(functionDefinition.assignment); + result.splice(i, 1); + hoistedFunctions.push(functionDefinition.assignment); + } + } + if (hoistedFunctions.length > 0) { + result.unshift(...hoistedFunctions); + } + return result; + } + + protected hoistVariableDeclarations(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] { + if (!scope.variableDeclarations) { + return statements; + } + + const result = statements.slice(); + const hoistedLocals: tstl.Identifier[] = []; + for (const declaration of scope.variableDeclarations) { + const symbols = declaration.left.map(i => i.symbolId).filter(s => s !== undefined); + if (symbols.some(s => this.shouldHoist(s, scope))) { + let assignment: tstl.AssignmentStatement | undefined; + if (declaration.right) { + assignment = tstl.createAssignmentStatement( + declaration.left, + declaration.right + ); + } + const i = result.indexOf(declaration); + if (i >= 0) { + if (assignment) { + result.splice(i, 1, assignment); + } else { + result.splice(i, 1); + } + } else { + // Special case for 'var's declared in child scopes + this.replaceStatementInParent(declaration, assignment); + } + hoistedLocals.push(...declaration.left); + } + } + if (hoistedLocals.length > 0) { + result.unshift(tstl.createVariableDeclarationStatement(hoistedLocals)); + } + return result; + } + + protected performHoisting(statements: tstl.Statement[]): tstl.Statement[] { + if (this.options.noHoisting) { + return statements; + } + + const scope = this.peekScope(); + + let result = this.hoistFunctionDefinitions(scope, statements); + + result = this.hoistVariableDeclarations(scope, result); + + return result; + } + protected popScope(): Scope { - return this.scopeStack.pop(); + const scope = this.scopeStack.pop(); + return scope; + } + + protected createHoistableVariableDeclarationStatement( + identifier: ts.Identifier, + initializer?: tstl.Expression, + tsOriginal?: ts.Node, + parent?: tstl.Node + ): tstl.AssignmentStatement | tstl.VariableDeclarationStatement + { + const variable = this.transformIdentifier(identifier); + const declaration = tstl.createVariableDeclarationStatement(variable, initializer, tsOriginal, parent); + if (!this.options.noHoisting && variable.symbolId) { + const scope = this.peekScope(); + if (!scope.variableDeclarations) { scope.variableDeclarations = []; } + scope.variableDeclarations.push(declaration); + } + return declaration; } private statementVisitResultToStatementArray(visitResult: StatementVisitResult): tstl.Statement[] { diff --git a/src/LuaTranspiler.ts b/src/LuaTranspiler.ts index c5831c420..bb49ce9dc 100644 --- a/src/LuaTranspiler.ts +++ b/src/LuaTranspiler.ts @@ -148,4 +148,4 @@ export class LuaTranspiler { console.log(`${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`); } } -} \ No newline at end of file +} diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 5da6dd37e..f4d847e0e 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -590,4 +590,26 @@ export class TSHelper { return false; } + + public static getFirstDeclaration(symbol: ts.Symbol, sourceFile?: ts.SourceFile): ts.Declaration | undefined { + let declarations = symbol.getDeclarations(); + if (!declarations) { + return undefined; + } + if (sourceFile) { + declarations = declarations.filter(d => this.findFirstNodeAbove(d, ts.isSourceFile) === sourceFile); + } + return declarations.length > 0 + ? declarations.reduce((p, c) => p.pos < c.pos ? p : c) + : undefined; + } + + public static isFirstDeclaration(node: ts.VariableDeclaration, checker: ts.TypeChecker): boolean { + const symbol = checker.getSymbolAtLocation(node.name); + if (!symbol) { + return false; + } + const firstDeclaration = this.getFirstDeclaration(symbol); + return firstDeclaration === node; + } } diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index e7f55d599..b5719bc90 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -119,4 +119,12 @@ export class TSTLErrors { "the TupleReturn decorator.", node); }; + + public static ReferencedBeforeDeclaration = (node: ts.Identifier) => { + return new TranspileError( + `Identifier "${node.text}" was referenced before it was declared. The declaration ` + + "must be moved before the identifier's use, or hoisting must be enabled.", + node + ); + } } diff --git a/test/src/util.ts b/test/src/util.ts index 34ba1458d..fe63b218f 100644 --- a/test/src/util.ts +++ b/test/src/util.ts @@ -98,6 +98,24 @@ export function transpileAndExecute( return executeLua(lua); } +export function transpileExecuteAndReturnExport( + tsStr: string, + returnExport: string, + compilerOptions?: CompilerOptions, + luaHeader?: string +): any +{ + const wrappedTsString = `declare function JSONStringify(p: any): string; + ${tsStr}`; + + const lua = `return (function() + ${luaHeader ? luaHeader : ""} + ${transpileString(wrappedTsString, compilerOptions, false)} + end)().${returnExport}`; + + return executeLua(lua); +} + export function parseTypeScript(typescript: string, target: LuaTarget = LuaTarget.Lua53) : [ts.SourceFile, ts.TypeChecker] { const compilerHost = { diff --git a/test/translation/lua/continue.lua b/test/translation/lua/continue.lua index ffb13df0c..58127ab1b 100644 --- a/test/translation/lua/continue.lua +++ b/test/translation/lua/continue.lua @@ -2,9 +2,9 @@ local i = 0; while i < 10 do do if i < 5 then - goto __continue0; + goto __continue1; end end - ::__continue0:: + ::__continue1:: i = i + 1; end diff --git a/test/translation/lua/continueConcurrent.lua b/test/translation/lua/continueConcurrent.lua index 60d60047e..f689b22bc 100644 --- a/test/translation/lua/continueConcurrent.lua +++ b/test/translation/lua/continueConcurrent.lua @@ -2,12 +2,12 @@ local i = 0; while i < 10 do do if i < 5 then - goto __continue0; + goto __continue1; end if i == 7 then - goto __continue0; + goto __continue1; end end - ::__continue0:: + ::__continue1:: i = i + 1; end diff --git a/test/translation/lua/continueNested.lua b/test/translation/lua/continueNested.lua index 88be74803..39e5f7307 100644 --- a/test/translation/lua/continueNested.lua +++ b/test/translation/lua/continueNested.lua @@ -2,19 +2,19 @@ local i = 0; while i < 5 do do if (i % 2) == 0 then - goto __continue0; + goto __continue1; end local j = 0; while j < 2 do do if j == 1 then - goto __continue1; + goto __continue3; end end - ::__continue1:: + ::__continue3:: j = j + 1; end end - ::__continue0:: + ::__continue1:: i = i + 1; end diff --git a/test/translation/lua/continueNestedConcurrent.lua b/test/translation/lua/continueNestedConcurrent.lua index 1e1ed1ca1..614cb2e0b 100644 --- a/test/translation/lua/continueNestedConcurrent.lua +++ b/test/translation/lua/continueNestedConcurrent.lua @@ -2,22 +2,22 @@ local i = 0; while i < 5 do do if (i % 2) == 0 then - goto __continue0; + goto __continue1; end local j = 0; while j < 2 do do if j == 1 then - goto __continue1; + goto __continue3; end end - ::__continue1:: + ::__continue3:: j = j + 1; end if i == 4 then - goto __continue0; + goto __continue1; end end - ::__continue0:: + ::__continue1:: i = i + 1; end diff --git a/test/translation/lua/do.lua b/test/translation/lua/do.lua index dcedbadcc..14639278d 100644 --- a/test/translation/lua/do.lua +++ b/test/translation/lua/do.lua @@ -3,5 +3,5 @@ repeat do e = e - 1; end - ::__continue0:: + ::__continue1:: until not (e > 0); diff --git a/test/translation/lua/for.lua b/test/translation/lua/for.lua index 23293c334..b81e83929 100644 --- a/test/translation/lua/for.lua +++ b/test/translation/lua/for.lua @@ -2,6 +2,6 @@ local i = 1; while i <= 100 do do end - ::__continue0:: + ::__continue1:: i = i + 1; end diff --git a/test/translation/lua/forIn.lua b/test/translation/lua/forIn.lua index 685c401c4..5882e705d 100644 --- a/test/translation/lua/forIn.lua +++ b/test/translation/lua/forIn.lua @@ -1,5 +1,5 @@ for i in pairs({a = 1, b = 2, c = 3, d = 4}) do do end - ::__continue0:: + ::__continue1:: end diff --git a/test/translation/lua/forOf.lua b/test/translation/lua/forOf.lua index 308f64684..ce8676017 100644 --- a/test/translation/lua/forOf.lua +++ b/test/translation/lua/forOf.lua @@ -3,5 +3,5 @@ for ____TS_index = 1, #____TS_array do local i = ____TS_array[____TS_index]; do end - ::__continue0:: + ::__continue1:: end diff --git a/test/translation/lua/modulesChangedVariableExport.lua b/test/translation/lua/modulesChangedVariableExport.lua index 2776db6f6..aa02323b6 100644 --- a/test/translation/lua/modulesChangedVariableExport.lua +++ b/test/translation/lua/modulesChangedVariableExport.lua @@ -1,4 +1,3 @@ local exports = exports or {}; -exports.test = nil; exports.test = 1; -return exports; \ No newline at end of file +return exports; diff --git a/test/translation/lua/tupleReturn.lua b/test/translation/lua/tupleReturn.lua index a75c85272..c8d492bec 100644 --- a/test/translation/lua/tupleReturn.lua +++ b/test/translation/lua/tupleReturn.lua @@ -1,28 +1,28 @@ -tupleReturn = function() - return 0, "foobar"; -end; -tupleReturn(); -noTupleReturn(); -local a, b = tupleReturn(); -local c, d = table.unpack(noTupleReturn()); -a, b = tupleReturn(); -c, d = table.unpack(noTupleReturn()); -local e = ({tupleReturn()}); -local f = noTupleReturn(); -e = ({tupleReturn()}); -f = noTupleReturn(); -foo(({tupleReturn()})); -foo(noTupleReturn()); -tupleReturnFromVar = function() - local r = {1, "baz"}; - return table.unpack(r); -end; -tupleReturnForward = function() - return tupleReturn(); -end; -tupleNoForward = function() - return ({tupleReturn()}); -end; -tupleReturnUnpack = function() - return table.unpack(tupleNoForward()); -end; +tupleReturn = function() + return 0, "foobar"; +end; +tupleReturn(); +noTupleReturn(); +local a, b = tupleReturn(); +local c, d = table.unpack(noTupleReturn()); +a, b = tupleReturn(); +c, d = table.unpack(noTupleReturn()); +local e = ({tupleReturn()}); +local f = noTupleReturn(); +e = ({tupleReturn()}); +f = noTupleReturn(); +foo(({tupleReturn()})); +foo(noTupleReturn()); +tupleReturnFromVar = function() + local r = {1, "baz"}; + return table.unpack(r); +end; +tupleReturnForward = function() + return tupleReturn(); +end; +tupleNoForward = function() + return ({tupleReturn()}); +end; +tupleReturnUnpack = function() + return table.unpack(tupleNoForward()); +end; diff --git a/test/translation/lua/while.lua b/test/translation/lua/while.lua index 5571f8880..a779e9ded 100644 --- a/test/translation/lua/while.lua +++ b/test/translation/lua/while.lua @@ -3,5 +3,5 @@ while d > 0 do do d = d - 1; end - ::__continue0:: + ::__continue1:: end diff --git a/test/unit/class.spec.ts b/test/unit/class.spec.ts index 928770ad6..384d12fdd 100644 --- a/test/unit/class.spec.ts +++ b/test/unit/class.spec.ts @@ -660,4 +660,20 @@ export class ClassTests { Expect(result).toBe(8); } + + @Test("Exported class super call") + public exportedClassSupercAll(): void { + const code = + `export class Foo { + prop: string; + constructor(prop: string) { this.prop = prop; } + } + export class Bar extends Foo { + constructor() { + super("bar"); + } + } + export const baz = (new Bar()).prop;`; + Expect(util.transpileExecuteAndReturnExport(code, "baz")).toBe("bar"); + } } diff --git a/test/unit/compiler/configuration/mixed/index.spec.ts b/test/unit/compiler/configuration/mixed/index.spec.ts index 376cd8c55..1268e1ba5 100644 --- a/test/unit/compiler/configuration/mixed/index.spec.ts +++ b/test/unit/compiler/configuration/mixed/index.spec.ts @@ -35,6 +35,7 @@ export class MixedConfigurationTests { // Only present in TSTL dfaults noHeader: optionDeclarations["noHeader"].default, project: tsConfigPath, + noHoisting: optionDeclarations["noHoisting"].default, } as CompilerOptions); } -} \ No newline at end of file +} diff --git a/test/unit/compiler/configuration/options.spec.ts b/test/unit/compiler/configuration/options.spec.ts index f1338b6b1..262b80085 100644 --- a/test/unit/compiler/configuration/options.spec.ts +++ b/test/unit/compiler/configuration/options.spec.ts @@ -26,4 +26,4 @@ export class ObjectLiteralTests Expect(result).toBe("local a = Map.new(true);"); } -} \ No newline at end of file +} diff --git a/test/unit/hoisting.spec.ts b/test/unit/hoisting.spec.ts new file mode 100644 index 000000000..8c3ebcaf7 --- /dev/null +++ b/test/unit/hoisting.spec.ts @@ -0,0 +1,228 @@ +import * as ts from "typescript"; +import { Expect, Test, TestCase } from "alsatian"; + +import * as util from "../src/util"; +import { CompilerOptions, LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; +import { TranspileError } from "../../src/TranspileError"; + +export class HoistingTests { + @Test("Var Hoisting") + public varHoisting(): void { + const code = + `foo = "foo"; + var foo; + return foo;`; + const result = util.transpileAndExecute(code); + Expect(result).toBe("foo"); + } + + @Test("Exported Var Hoisting") + public exportedVarHoisting(): void { + const code = + `foo = "foo"; + export var foo;`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("foo"); + } + + @TestCase("let") + @TestCase("const") + @Test("Let/Const Hoisting") + public letConstHoisting(varType: string): void { + const code = + `let bar: string; + function setBar() { bar = foo; } + ${varType} foo = "foo"; + setBar(); + return foo;`; + const result = util.transpileAndExecute(code); + Expect(result).toBe("foo"); + } + + @TestCase("let") + @TestCase("const") + @Test("Exported Let/Const Hoisting") + public exportedLetConstHoisting(varType: string): void { + const code = + `let bar: string; + function setBar() { bar = foo; } + export ${varType} foo = "foo"; + setBar();`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("foo"); + } + + @Test("Global Function Hoisting") + public globalFunctionHoisting(): void { + const code = + `const foo = bar(); + function bar() { return "bar"; } + return foo;`; + const result = util.transpileAndExecute(code); + Expect(result).toBe("bar"); + } + + @Test("Local Function Hoisting") + public localFunctionHoisting(): void { + const code = + `export const foo = bar(); + function bar() { return "bar"; }`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("bar"); + } + + @Test("Exported Function Hoisting") + public exportedFunctionHoisting(): void { + const code = + `const foo = bar(); + export function bar() { return "bar"; } + export const baz = foo;`; + const result = util.transpileExecuteAndReturnExport(code, "baz"); + Expect(result).toBe("bar"); + } + + @Test("Namespace Function Hoisting") + public namespaceFunctionHoisting(): void { + const code = + `let foo: string; + namespace NS { + foo = bar(); + function bar() { return "bar"; } + }`; + const result = util.transpileAndExecute("return foo;", undefined, undefined, code); + Expect(result).toBe("bar"); + } + + @Test("Exported Namespace Function Hoisting") + public exportedNamespaceFunctionHoisting(): void { + const code = + `let foo: string; + namespace NS { + foo = bar(); + export function bar() { return "bar"; } + }`; + const result = util.transpileAndExecute("return foo;", undefined, undefined, code); + Expect(result).toBe("bar"); + } + + @TestCase("var", "foo") + @TestCase("let", "bar") + @TestCase("const", "bar") + @Test("Hoisting in Non-Function Scope") + public hoistingInNonFunctionScope(varType: string, expectResult: string): void { + const code = + `function foo() { + ${varType} bar = "bar"; + for (let i = 0; i < 1; ++i) { + ${varType} bar = "foo"; + } + return bar; + } + return foo();`; + const result = util.transpileAndExecute(code); + Expect(result).toBe(expectResult); + } + + @Test("Namespace Hoisting") + public namespaceHoisting(): void { + const code = + `function bar() { + return NS.foo; + } + namespace NS { + export let foo = "foo"; + } + export const foo = bar();`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("foo"); + } + + @Test("Exported Namespace Hoisting") + public exportedNamespaceHoisting(): void { + const code = + `function bar() { + return NS.foo; + } + export namespace NS { + export let foo = "foo"; + } + export const foo = bar();`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("foo"); + } + + @Test("Nested Namespace Hoisting") + public nestedNamespaceHoisting(): void { + const code = + `export namespace Outer { + export function bar() { + return Inner.foo; + } + namespace Inner { + export let foo = "foo"; + } + } + export const foo = Outer.bar();`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("foo"); + } + + @Test("Class Hoisting") + public classHoisting(): void { + const code = + `function makeFoo() { + return new Foo(); + } + class Foo { + public bar = "foo"; + } + export const foo = makeFoo().bar;`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("foo"); + } + + @Test("Enum Hoisting") + public enumHoisting(): void { + const code = + `function bar() { + return E.A; + } + enum E { + A = "foo" + } + export const foo = bar();`; + const result = util.transpileExecuteAndReturnExport(code, "foo"); + Expect(result).toBe("foo"); + } + + @TestCase(`foo = "foo"; var foo;`, "foo") + @TestCase(`foo = "foo"; export var foo;`, "foo") + @TestCase(`function setBar() { const bar = foo; } let foo = "foo";`, "foo") + @TestCase(`function setBar() { const bar = foo; } const foo = "foo";`, "foo") + @TestCase(`function setBar() { const bar = foo; } export let foo = "foo";`, "foo") + @TestCase(`function setBar() { const bar = foo; } export const foo = "foo";`, "foo") + @TestCase(`const foo = bar(); function bar() { return "bar"; }`, "bar") + @TestCase(`export const foo = bar(); function bar() { return "bar"; }`, "bar") + @TestCase(`const foo = bar(); export function bar() { return "bar"; }`, "bar") + @TestCase(`function bar() { return NS.foo; } namespace NS { export let foo = "foo"; }`, "NS") + @TestCase( + `export namespace O { export function f() { return I.foo; } namespace I { export let foo = "foo"; } }`, + "I" + ) + @TestCase(`function makeFoo() { return new Foo(); } class Foo {}`, "Foo") + @TestCase(`function bar() { return E.A; } enum E { A = "foo" }`, "E") + @Test("No Hoisting") + public noHoisting(code: string, identifier: string): void { + const compilerOptions: CompilerOptions = { + noHoisting: true, + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + Expect(() => util.transpileString(code, compilerOptions)).toThrowError( + TranspileError, + `Identifier "${identifier}" was referenced before it was declared. The declaration ` + + "must be moved before the identifier's use, or hoisting must be enabled." + ); + } +} diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index 06992bc77..1537d80b5 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -694,10 +694,10 @@ export class LuaLoopTests const luajit = util.transpileString(loop, { luaTarget: LuaTarget.LuaJIT }); // Assert - Expect(lua51.indexOf("::__continue0::") !== -1).toBe(false); // No labels in 5.1 - Expect(lua52.indexOf("::__continue0::") !== -1).toBe(true); // Labels from 5.2 onwards - Expect(lua53.indexOf("::__continue0::") !== -1).toBe(true); - Expect(luajit.indexOf("::__continue0::") !== -1).toBe(true); + Expect(lua51.indexOf("::__continue1::") !== -1).toBe(false); // No labels in 5.1 + Expect(lua52.indexOf("::__continue1::") !== -1).toBe(true); // Labels from 5.2 onwards + Expect(lua53.indexOf("::__continue1::") !== -1).toBe(true); + Expect(luajit.indexOf("::__continue1::") !== -1).toBe(true); } @Test("for dead code after return")