Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 71 additions & 170 deletions src/LuaAST.ts

Large diffs are not rendered by default.

7 changes: 2 additions & 5 deletions src/LuaPrinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,13 +395,10 @@ export class LuaPrinter {
return this.createSourceNode(statement, chunks);
}

public printIfStatement(statement: lua.IfStatement): SourceNode {
public printIfStatement(statement: lua.IfStatement, isElseIf = false): SourceNode {
const chunks: SourceChunk[] = [];

const isElseIf = statement.parent !== undefined && lua.isIfStatement(statement.parent);

const prefix = isElseIf ? "elseif" : "if";

chunks.push(this.indent(prefix + " "), this.printExpression(statement.condition), " then\n");

this.pushIndent();
Expand All @@ -410,7 +407,7 @@ export class LuaPrinter {

if (statement.elseBlock) {
if (lua.isIfStatement(statement.elseBlock)) {
chunks.push(this.printIfStatement(statement.elseBlock));
chunks.push(this.printIfStatement(statement.elseBlock, true));
} else {
chunks.push(this.indent("else\n"));
this.pushIndent();
Expand Down
3 changes: 3 additions & 0 deletions src/transformation/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,6 @@ export const InvalidAmbientIdentifierName = (node: ts.Identifier) =>

export const InvalidForRangeCall = (node: ts.Node, message: string) =>
new TranspileError(`Invalid @forRange call: ${message}`, node);

export const UnsupportedVarDeclaration = (node: ts.Node) =>
new TranspileError("`var` declarations are not supported. Use `let` or `const` instead.", node);
63 changes: 14 additions & 49 deletions src/transformation/utils/lua-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { LuaTarget } from "../../CompilerOptions";
import * as lua from "../../LuaAST";
import { TransformationContext } from "../context";
import { getCurrentNamespace } from "../visitors/namespace";
import { UndefinedScope } from "./errors";
import { createExportedIdentifier, getIdentifierExportScope } from "./export";
import { findScope, peekScope, ScopeType } from "./scope";
import { isFirstDeclaration, isFunctionType } from "./typescript";
import { isFunctionType } from "./typescript";

export type OneToManyVisitorResult<T extends lua.Node> = T | T[] | undefined;
export function unwrapVisitorResult<T extends lua.Node>(result: OneToManyVisitorResult<T>): T[] {
Expand All @@ -27,22 +26,6 @@ export function createExportsIdentifier(): lua.Identifier {
return lua.createIdentifier("____exports");
}

export function replaceStatementInParent(oldNode: lua.Statement, newNode?: lua.Statement): void {
if (!oldNode.parent) {
throw new Error("node has not yet been assigned a parent");
}

if (lua.isBlock(oldNode.parent) || lua.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");
}
}

export function createExpressionPlusOne(expression: lua.Expression): lua.Expression {
if (lua.isNumericLiteral(expression)) {
const newNode = lua.cloneNode(expression);
Expand Down Expand Up @@ -111,10 +94,9 @@ export function createHoistableVariableDeclarationStatement(
context: TransformationContext,
identifier: lua.Identifier,
initializer?: lua.Expression,
tsOriginal?: ts.Node,
parent?: lua.Node
tsOriginal?: ts.Node
): lua.AssignmentStatement | lua.VariableDeclarationStatement {
const declaration = lua.createVariableDeclarationStatement(identifier, initializer, tsOriginal, parent);
const declaration = lua.createVariableDeclarationStatement(identifier, initializer, tsOriginal);
if (!context.options.noHoisting && identifier.symbolId) {
const scope = peekScope(context);
if (!scope.variableDeclarations) {
Expand All @@ -132,13 +114,13 @@ export function createLocalOrExportedOrGlobalDeclaration(
lhs: lua.Identifier | lua.Identifier[],
rhs?: lua.Expression | lua.Expression[],
tsOriginal?: ts.Node,
parent?: lua.Node,
overrideExportScope?: ts.SourceFile | ts.ModuleDeclaration
): lua.Statement[] {
let declaration: lua.VariableDeclarationStatement | undefined;
let assignment: lua.AssignmentStatement | undefined;

const functionDeclaration = tsOriginal && ts.isFunctionDeclaration(tsOriginal) ? tsOriginal : undefined;
const isVariableDeclaration = tsOriginal !== undefined && ts.isVariableDeclaration(tsOriginal);
const isFunctionDeclaration = tsOriginal !== undefined && ts.isFunctionDeclaration(tsOriginal);

const identifiers = Array.isArray(lhs) ? lhs : [lhs];
if (identifiers.length === 0) {
Expand All @@ -154,48 +136,31 @@ export function createLocalOrExportedOrGlobalDeclaration(
assignment = lua.createAssignmentStatement(
identifiers.map(identifier => createExportedIdentifier(context, identifier, exportScope)),
rhs,
tsOriginal,
parent
tsOriginal
);
}
} else {
const insideFunction = findScope(context, ScopeType.Function) !== undefined;
let isLetOrConst = false;
let isVariableFirstDeclaration = true; // var can have multiple declarations for the same variable :/
if (tsOriginal && ts.isVariableDeclaration(tsOriginal) && tsOriginal.parent) {
isLetOrConst = (tsOriginal.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
isVariableFirstDeclaration = isLetOrConst || isFirstDeclaration(context, tsOriginal);
}

if (
(context.isModule || getCurrentNamespace(context) || insideFunction || isLetOrConst) &&
isVariableFirstDeclaration
) {
if (context.isModule || getCurrentNamespace(context) || insideFunction || isVariableDeclaration) {
// local
const isPossibleWrappedFunction =
!functionDeclaration &&
!isFunctionDeclaration &&
tsOriginal &&
ts.isVariableDeclaration(tsOriginal) &&
tsOriginal.initializer &&
isFunctionType(context, context.checker.getTypeAtLocation(tsOriginal.initializer));
if (isPossibleWrappedFunction) {
// Split declaration and assignment for wrapped function types to allow recursion
declaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal, parent);
assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal, parent);
declaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal);
assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal);
} else {
declaration = lua.createVariableDeclarationStatement(lhs, rhs, tsOriginal, parent);
declaration = lua.createVariableDeclarationStatement(lhs, rhs, tsOriginal);
}

if (!context.options.noHoisting) {
// Remember local variable declarations for hoisting later
const scope =
isLetOrConst || functionDeclaration
? peekScope(context)
: findScope(context, ScopeType.Function | ScopeType.File);

if (scope === undefined) {
throw UndefinedScope();
}
const scope = peekScope(context);

if (!scope.variableDeclarations) {
scope.variableDeclarations = [];
Expand All @@ -205,13 +170,13 @@ export function createLocalOrExportedOrGlobalDeclaration(
}
} else if (rhs) {
// global
assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal, parent);
assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal);
} else {
return [];
}
}

if (!context.options.noHoisting && functionDeclaration) {
if (!context.options.noHoisting && isFunctionDeclaration) {
// Remember function definitions for hoisting later
const functionSymbolId = (lhs as lua.Identifier).symbolId;
const scope = peekScope(context);
Expand Down
14 changes: 5 additions & 9 deletions src/transformation/utils/scope.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as assert from "assert";
import * as ts from "typescript";
import * as lua from "../../LuaAST";
import { getOrUpdate, isNonNull } from "../../utils";
import { TransformationContext } from "../context";
import { UndefinedFunctionDefinition, UndefinedScope } from "./errors";
import { replaceStatementInParent } from "./lua-ast";
import { getSymbolInfo } from "./symbols";
import { getFirstDeclarationInFile } from "./typescript";

Expand Down Expand Up @@ -168,15 +168,11 @@ function hoistVariableDeclarations(
}

const index = result.indexOf(declaration);
if (index >= 0) {
if (assignment) {
result.splice(index, 1, assignment);
} else {
result.splice(index, 1);
}
assert(index > -1);
if (assignment) {
result.splice(index, 1, assignment);
} else {
// Special case for 'var's declared in child scopes
replaceStatementInParent(declaration, assignment);
result.splice(index, 1);
}

hoistedLocals.push(...declaration.left);
Expand Down
1 change: 0 additions & 1 deletion src/transformation/visitors/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export const transformEnumDeclaration: FunctionVisitor<ts.EnumDeclaration> = (no
: lua.createIdentifier(member.name.getText(), member.name),
valueExpression,
node,
undefined,
exportScope
)
);
Expand Down
2 changes: 1 addition & 1 deletion src/transformation/visitors/loops/do-while.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ts from "typescript";
import * as lua from "../../../LuaAST";
import { FunctionVisitor } from "../../context";
import { transformLoopBody } from "./body";
import { transformLoopBody } from "./utils";

export const transformWhileStatement: FunctionVisitor<ts.WhileStatement> = (statement, context) => {
return lua.createWhileStatement(
Expand Down
14 changes: 8 additions & 6 deletions src/transformation/visitors/loops/for-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FunctionVisitor } from "../../context";
import { ForbiddenForIn, UnsupportedForInVariable } from "../../utils/errors";
import { isArrayType } from "../../utils/typescript";
import { transformIdentifier } from "../identifier";
import { transformLoopBody } from "./body";
import { getVariableDeclarationBinding, transformLoopBody } from "./utils";

export const transformForInStatement: FunctionVisitor<ts.ForInStatement> = (statement, context) => {
if (isArrayType(context, context.checker.getTypeAtLocation(statement.expression))) {
Expand All @@ -22,11 +22,13 @@ export const transformForInStatement: FunctionVisitor<ts.ForInStatement> = (stat
// TODO: After the transformation pipeline refactor we should look at refactoring this together with the
// for-of initializer transformation.
let iterationVariable: lua.Identifier;
if (
ts.isVariableDeclarationList(statement.initializer) &&
ts.isIdentifier(statement.initializer.declarations[0].name)
) {
iterationVariable = transformIdentifier(context, statement.initializer.declarations[0].name);
if (ts.isVariableDeclarationList(statement.initializer)) {
const binding = getVariableDeclarationBinding(statement.initializer);
if (!ts.isIdentifier(binding)) {
throw UnsupportedForInVariable(statement.initializer);
}

iterationVariable = transformIdentifier(context, binding);
} else if (ts.isIdentifier(statement.initializer)) {
// Iteration variable becomes ____key
iterationVariable = lua.createIdentifier("____key");
Expand Down
36 changes: 18 additions & 18 deletions src/transformation/visitors/loops/for-of.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,24 @@ import { isArrayType, isNumberType } from "../../utils/typescript";
import { transformArguments } from "../call";
import { transformIdentifier } from "../identifier";
import { transformArrayBindingElement, transformVariableDeclaration } from "../variable-declaration";
import { transformLoopBody } from "./body";
import { getVariableDeclarationBinding, transformLoopBody } from "./utils";

function transformForOfInitializer(
context: TransformationContext,
initializer: ts.ForInitializer,
expression: lua.Expression
): lua.Statement | undefined {
if (ts.isVariableDeclarationList(initializer)) {
const binding = getVariableDeclarationBinding(initializer);
// Declaration of new variable
if (ts.isArrayBindingPattern(initializer.declarations[0].name)) {
if (initializer.declarations[0].name.elements.length === 0) {
if (ts.isArrayBindingPattern(binding)) {
if (binding.elements.length === 0) {
// Ignore empty destructuring assignment
return undefined;
}

expression = createUnpackCall(context, expression, initializer);
} else if (ts.isObjectBindingPattern(initializer.declarations[0].name)) {
} else if (ts.isObjectBindingPattern(binding)) {
throw UnsupportedObjectDestructuringInForOf(initializer);
}

Expand Down Expand Up @@ -90,19 +91,19 @@ function transformForRangeStatement(
throw InvalidForRangeCall(statement.initializer, "@forRange loop must declare its own control variable.");
}

const controlDeclaration = statement.initializer.declarations[0];
if (!ts.isIdentifier(controlDeclaration.name)) {
const binding = getVariableDeclarationBinding(statement.initializer);
if (!ts.isIdentifier(binding)) {
throw InvalidForRangeCall(statement.initializer, "@forRange loop cannot use destructuring.");
}

if (!isNumberType(context, context.checker.getTypeAtLocation(controlDeclaration))) {
if (!isNumberType(context, context.checker.getTypeAtLocation(binding))) {
throw InvalidForRangeCall(
statement.expression,
"@forRange function must return Iterable<number> or Array<number>."
);
}

const control = transformIdentifier(context, controlDeclaration.name);
const control = transformIdentifier(context, binding);
const signature = context.checker.getResolvedSignature(statement.expression);
const [start, limit, step] = transformArguments(context, statement.expression.arguments, signature);
return lua.createForStatement(block, control, start, limit, step, statement);
Expand All @@ -121,12 +122,9 @@ function transformForOfLuaIteratorStatement(
if (ts.isVariableDeclarationList(statement.initializer)) {
// Variables declared in for loop
// for ${initializer} in ${iterable} do
const initializerVariable = statement.initializer.declarations[0].name;
if (ts.isArrayBindingPattern(initializerVariable)) {
const identifiers = castEach(
initializerVariable.elements.map(e => transformArrayBindingElement(context, e)),
lua.isIdentifier
);
const binding = getVariableDeclarationBinding(statement.initializer);
if (ts.isArrayBindingPattern(binding)) {
const identifiers = binding.elements.map(e => transformArrayBindingElement(context, e));
if (identifiers.length === 0) {
identifiers.push(lua.createAnonymousIdentifier());
}
Expand Down Expand Up @@ -163,6 +161,7 @@ function transformForOfLuaIteratorStatement(
// LuaIterator (no TupleReturn)
if (
ts.isVariableDeclarationList(statement.initializer) &&
statement.initializer.declarations.length > 0 &&
ts.isIdentifier(statement.initializer.declarations[0].name)
) {
// Single variable declared in for loop
Expand Down Expand Up @@ -194,15 +193,15 @@ function transformForOfArrayStatement(
let valueVariable: lua.Identifier;
if (ts.isVariableDeclarationList(statement.initializer)) {
// Declaration of new variable
const variables = statement.initializer.declarations[0].name;
if (ts.isArrayBindingPattern(variables) || ts.isObjectBindingPattern(variables)) {
const binding = getVariableDeclarationBinding(statement.initializer);
if (ts.isArrayBindingPattern(binding) || ts.isObjectBindingPattern(binding)) {
valueVariable = lua.createIdentifier("____values");
const initializer = transformForOfInitializer(context, statement.initializer, valueVariable);
if (initializer) {
block.statements.unshift(initializer);
}
} else {
valueVariable = transformIdentifier(context, variables);
valueVariable = transformIdentifier(context, binding);
}
} else {
// Assignment to existing variable
Expand All @@ -228,13 +227,14 @@ function transformForOfIteratorStatement(
const iterable = context.transformExpression(statement.expression);
if (
ts.isVariableDeclarationList(statement.initializer) &&
statement.initializer.declarations.length > 0 &&
ts.isIdentifier(statement.initializer.declarations[0].name)
) {
// Single variable declared in for loop
// for ${initializer} in __TS__iterator(${iterator}) do
return lua.createForInStatement(
block,
[transformIdentifier(context, statement.initializer.declarations[0].name as ts.Identifier)],
[transformIdentifier(context, statement.initializer.declarations[0].name)],
[transformLuaLibFunction(context, LuaLibFeature.Iterator, statement.expression, iterable)]
);
} else {
Expand Down
5 changes: 3 additions & 2 deletions src/transformation/visitors/loops/for.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as ts from "typescript";
import * as lua from "../../../LuaAST";
import { FunctionVisitor } from "../../context";
import { transformVariableDeclaration } from "../variable-declaration";
import { transformLoopBody } from "./body";
import { checkVariableDeclarationList, transformVariableDeclaration } from "../variable-declaration";
import { transformLoopBody } from "./utils";

export const transformForStatement: FunctionVisitor<ts.ForStatement> = (statement, context) => {
const result: lua.Statement[] = [];

if (statement.initializer) {
if (ts.isVariableDeclarationList(statement.initializer)) {
checkVariableDeclarationList(statement.initializer);
// local initializer = value
result.push(...statement.initializer.declarations.flatMap(d => transformVariableDeclaration(context, d)));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as lua from "../../../LuaAST";
import { TransformationContext } from "../../context";
import { performHoisting, popScope, pushScope, ScopeType } from "../../utils/scope";
import { transformBlockOrStatement } from "../block";
import { checkVariableDeclarationList } from "../variable-declaration";

export function transformLoopBody(
context: TransformationContext,
Expand All @@ -23,3 +24,13 @@ export function transformLoopBody(

return baseResult;
}

export function getVariableDeclarationBinding(node: ts.VariableDeclarationList): ts.BindingName {
checkVariableDeclarationList(node);

if (node.declarations.length === 0) {
return ts.createIdentifier("____");
}

return node.declarations[0].name;
}
Loading