diff --git a/build_lualib.ts b/build_lualib.ts index e43e9b000..fa4034baa 100644 --- a/build_lualib.ts +++ b/build_lualib.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import * as glob from "glob"; import {compile} from "./src/Compiler"; +import {LuaLib as luaLib, LuaLibFeature} from "./src/LuaLib"; const bundlePath = "./dist/lualib/lualib_bundle.lua"; @@ -15,14 +16,11 @@ compile([ "--rootDir", "./src/lualib", ...glob.sync("./src/lualib/*.ts"), - ]); +]); if (fs.existsSync(bundlePath)) { - fs.unlinkSync(bundlePath); + fs.unlinkSync(bundlePath); } -let bundle = ""; - -glob.sync("./dist/lualib/*.lua").forEach(fileName => bundle += fs.readFileSync(fileName)); - +const bundle = luaLib.loadFeatures(Object.keys(LuaLibFeature).map(lib => LuaLibFeature[lib])); fs.writeFileSync(bundlePath, bundle); diff --git a/src/Decorator.ts b/src/Decorator.ts index 870cc9344..9bbe02a26 100644 --- a/src/Decorator.ts +++ b/src/Decorator.ts @@ -14,6 +14,7 @@ export class Decorator { case "phantom": return DecoratorKind.Phantom; case "tuplereturn": return DecoratorKind.TupleReturn; case "noclassor": return DecoratorKind.NoClassOr; + case "luaiterator": return DecoratorKind.LuaIterator; } return undefined; @@ -37,4 +38,5 @@ export enum DecoratorKind { Phantom = "Phantom", TupleReturn = "TupleReturn", NoClassOr = "NoClassOr", + LuaIterator = "LuaIterator", } diff --git a/src/Errors.ts b/src/Errors.ts index 7d9973342..556bf4775 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -109,4 +109,11 @@ export class TSTLErrors { node); } } + + public static UnsupportedNonDestructuringLuaIterator = (node: ts.Node) => { + return new TranspileError("Unsupported use of lua iterator with TupleReturn decorator in for...of statement. " + + "You must use a destructuring statement to catch results from a lua iterator with " + + "the TupleReturn decorator.", + node); + } } diff --git a/src/LuaLib.ts b/src/LuaLib.ts new file mode 100644 index 000000000..467131acb --- /dev/null +++ b/src/LuaLib.ts @@ -0,0 +1,60 @@ +import * as fs from "fs"; +import * as path from "path"; + +export enum LuaLibFeature { + ArrayConcat = "ArrayConcat", + ArrayEvery = "ArrayEvery", + ArrayFilter = "ArrayFilter", + ArrayForEach = "ArrayForEach", + ArrayIndexOf = "ArrayIndexOf", + ArrayMap = "ArrayMap", + ArrayPush = "ArrayPush", + ArrayReverse = "ArrayReverse", + ArrayShift = "ArrayShift", + ArrayUnshift = "ArrayUnshift", + ArraySort = "ArraySort", + ArraySlice = "ArraySlice", + ArraySome = "ArraySome", + ArraySplice = "ArraySplice", + FunctionApply = "FunctionApply", + FunctionBind = "FunctionBind", + FunctionCall = "FunctionCall", + InstanceOf = "InstanceOf", + Iterator = "Iterator", + Map = "Map", + Set = "Set", + StringReplace = "StringReplace", + StringSplit = "StringSplit", + Symbol = "Symbol", + Ternary = "Ternary", +} + +const luaLibDependencies: { [lib in LuaLibFeature]?: LuaLibFeature[] } = { + Iterator: [LuaLibFeature.Symbol], + Map: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], + Set: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], +}; + +export class LuaLib { + public static loadFeatures(features: Iterable): string { + let result = ""; + + const loadedFeatures = new Set(); + + function load(feature: LuaLibFeature): void { + if (!loadedFeatures.has(feature)) { + loadedFeatures.add(feature); + if (luaLibDependencies[feature]) { + luaLibDependencies[feature].forEach(load); + } + const featureFile = path.resolve(__dirname, `../dist/lualib/${feature}.lua`); + result += fs.readFileSync(featureFile).toString() + "\n"; + } + } + + for (const feature of features) { + load(feature); + } + return result; + } +} diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 7a69a2b59..44b49a42e 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -138,6 +138,16 @@ export class TSHelper { return this.forTypeOrAnySupertype(type, checker, t => this.isExplicitArrayType(t, checker)); } + public static isLuaIteratorCall(node: ts.Node, checker: ts.TypeChecker): boolean { + if (ts.isCallExpression(node) && node.parent && ts.isForOfStatement(node.parent)) { + const type = checker.getTypeAtLocation(node.expression); + return this.getCustomDecorators(type, checker) + .has(DecoratorKind.LuaIterator); + } else { + return false; + } + } + public static isTupleReturnCall(node: ts.Node, checker: ts.TypeChecker): boolean { if (ts.isCallExpression(node)) { const type = checker.getTypeAtLocation(node.expression); @@ -157,7 +167,9 @@ export class TSHelper { checker.getTypeAtLocation(declaration), checker ); - return decorators.has(DecoratorKind.TupleReturn); + return decorators.has(DecoratorKind.TupleReturn) + // Lua iterators are not 'true' tupleReturn functions as they actually return a function + && !decorators.has(DecoratorKind.LuaIterator); } else { return false; } diff --git a/src/Transpiler.ts b/src/Transpiler.ts index 3dc736649..561733492 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -1,10 +1,10 @@ -import * as fs from "fs"; import * as path from "path"; import * as ts from "typescript"; import { CompilerOptions } from "./CompilerOptions"; import { DecoratorKind } from "./Decorator"; import { TSTLErrors } from "./Errors"; +import { LuaLib as luaLib, LuaLibFeature } from "./LuaLib"; import { ContextType, TSHelper as tsHelper } from "./TSHelper"; /* tslint:disable */ @@ -18,32 +18,6 @@ export enum LuaTarget { LuaJIT = "jit", } -export enum LuaLibFeature { - ArrayConcat = "ArrayConcat", - ArrayEvery = "ArrayEvery", - ArrayFilter = "ArrayFilter", - ArrayForEach = "ArrayForEach", - ArrayIndexOf = "ArrayIndexOf", - ArrayMap = "ArrayMap", - ArrayPush = "ArrayPush", - ArrayReverse = "ArrayReverse", - ArrayShift = "ArrayShift", - ArrayUnshift = "ArrayUnshift", - ArraySort = "ArraySort", - ArraySlice = "ArraySlice", - ArraySome = "ArraySome", - ArraySplice = "ArraySplice", - FunctionApply = "FunctionApply", - FunctionBind = "FunctionBind", - FunctionCall = "FunctionCall", - InstanceOf = "InstanceOf", - Map = "Map", - Set = "Set", - StringReplace = "StringReplace", - StringSplit = "StringSplit", - Ternary = "Ternary", -} - export enum LuaLibImportKind { None = "none", Always = "always", @@ -181,11 +155,6 @@ export abstract class LuaTranspiler { } public importLuaLibFeature(feature: LuaLibFeature): void { - // Add additional lib requirements - if (feature === LuaLibFeature.Map || feature === LuaLibFeature.Set) { - this.luaLibFeatureSet.add(LuaLibFeature.InstanceOf); - } - // TODO inline imported features in output i option set this.luaLibFeatureSet.add(feature); } @@ -215,7 +184,7 @@ export abstract class LuaTranspiler { return filePath.replace(new RegExp("\\\\|\/", "g"), "."); } - public computeEnumMembers(node: ts.EnumDeclaration): Array<{ name: string, value: string | number }> { + public computeEnumMembers(node: ts.EnumDeclaration): Array<{ name: ts.PropertyName, value: string | number }> { let val: number | string = 0; let hasStringInitializers = false; @@ -233,7 +202,7 @@ export abstract class LuaTranspiler { throw TSTLErrors.HeterogeneousEnum(node); } - const enumMember = { name: this.transpileIdentifier(member.name as ts.Identifier), value: val }; + const enumMember = { name: member.name, value: val }; if (typeof val === "number") { val++; @@ -266,10 +235,7 @@ export abstract class LuaTranspiler { // Inline lualib features if (this.options.luaLibImport === LuaLibImportKind.Inline && this.luaLibFeatureSet.size > 0) { result += "\n" + "-- Lua Library Imports\n"; - for (const feature of this.luaLibFeatureSet) { - const featureFile = path.resolve(__dirname, `../dist/lualib/${feature}.lua`); - result += fs.readFileSync(featureFile).toString() + "\n"; - } + result += luaLib.loadFeatures(this.luaLibFeatureSet); } if (this.isModule) { @@ -469,11 +435,14 @@ export abstract class LuaTranspiler { } this.computeEnumMembers(node).forEach(enumMember => { + const name = this.transpilePropertyName(enumMember.name); if (membersOnly) { - const defName = this.definitionName(enumMember.name); + const defName = this.definitionName(name); result += this.indent + `${defName}=${enumMember.value}\n`; } else { - const defName = `${this.transpileIdentifier(node.name)}.${enumMember.name}`; + const defName = ts.isIdentifier(enumMember.name) + ? `${this.transpileIdentifier(node.name)}.${name}` + : `${this.transpileIdentifier(node.name)}[${name}]`; result += this.indent + `${defName}=${enumMember.value}\n`; } }); @@ -584,35 +553,80 @@ export abstract class LuaTranspiler { } public transpileForOf(node: ts.ForOfStatement): string { - // Get variable identifier - const variable = (node.initializer as ts.VariableDeclarationList).declarations[0]; - // Transpile expression const iterable = this.transpileExpression(node.expression); - // Use ipairs for array types, pairs otherwise - const isArray = tsHelper.isArrayType(this.checker.getTypeAtLocation(node.expression), this.checker); - let result = ""; - if (!isArray && ts.isIdentifier(variable.name)) { - result = this.indent + `for _, ${this.transpileIdentifier(variable.name)} in pairs(${iterable}) do\n`; + let itemVariable: ts.Expression; + if (tsHelper.isArrayType(this.checker.getTypeAtLocation(node.expression), this.checker)) { + // Arrays use numeric for loop (performs better than ipairs) + const indexVariable = `____TS_index`; + if (!ts.isIdentifier(node.expression)) { + // Cache expression + const arrayVariable = `____TS_array`; + result += this.indent + `local ${arrayVariable} = ${iterable};\n`; + result += this.indent + `for ${indexVariable}=1, #${arrayVariable} do\n`; + itemVariable = ts.createIdentifier(`${arrayVariable}[${indexVariable}]`); + } else { + result += this.indent + `for ${indexVariable}=1, #${iterable} do\n`; + itemVariable = ts.createIdentifier(`${iterable}[${indexVariable}]`); + } + } else { - let itemVariable: ts.Identifier; - if (isArray) { - // Cache the expression result - result += this.indent + `local __loopVariable${this.genVarCounter} = ${iterable};\n`; - result += this.indent + `for i${this.genVarCounter}=1, #__loopVariable${this.genVarCounter} do\n`; - itemVariable = ts.createIdentifier(`__loopVariable${this.genVarCounter}[i${this.genVarCounter}]`); + // Custom iterators + let variableName: string; + const isLuaIterator = tsHelper.isLuaIteratorCall(node.expression, this.checker); + if (isLuaIterator && tsHelper.isTupleReturnCall(node.expression, this.checker)) { + if (ts.isVariableDeclarationList(node.initializer)) { + // Variables declared in for loop + if (!ts.isIdentifier(node.initializer.declarations[0].name)) { + variableName = (node.initializer.declarations[0].name as ts.ArrayBindingPattern).elements + .map(e => this.transpileArrayBindingElement(e)).join(", "); + } else { + // Single variable is not allowed + throw TSTLErrors.UnsupportedNonDestructuringLuaIterator(node.initializer); + } + } else { + // Variables NOT declared in for loop - catch iterator values in temps and assign + if (ts.isArrayLiteralExpression(node.initializer)) { + const tmps = node.initializer.elements.map((_, i) => `____TS_value${i}`); + itemVariable = ts.createArrayLiteral(tmps.map(tmp => ts.createIdentifier(tmp))); + variableName = tmps.join(", "); + } else { + // Single variable is not allowed + throw TSTLErrors.UnsupportedNonDestructuringLuaIterator(node.initializer); + } + } } else { - const variableName = `__forOfValue${this.genVarCounter}`; - itemVariable = ts.createIdentifier(variableName); - result += this.indent + `for _, ${variableName} in pairs(${iterable}) do\n`; + if (ts.isVariableDeclarationList(node.initializer) + && ts.isIdentifier(node.initializer.declarations[0].name)) { + // Single variable declared in for loop + variableName = this.transpileExpression(node.initializer.declarations[0].name); + } else { + // Destructuring or variable NOT declared in for loop + variableName = "____TS_value"; + itemVariable = ts.createIdentifier(variableName); + } } - const declaration = ts.createVariableDeclaration(variable.name, undefined, itemVariable); + const iterator = isLuaIterator ? iterable : this.transpileLuaLibFunction(LuaLibFeature.Iterator, iterable); + result = this.indent + `for ${variableName} in ${iterator} do\n`; + } + + if (itemVariable) { this.pushIndent(); - result += this.indent + this.transpileVariableDeclaration(declaration) + ";\n"; + if (ts.isVariableDeclarationList(node.initializer)) { + // Declare item variable + const declaration = ts.createVariableDeclaration(node.initializer.declarations[0].name, + undefined, + itemVariable); + result += this.indent + this.transpileVariableDeclaration(declaration) + ";\n"; + } else { + // Assign item variable + const assignment = ts.createAssignment(node.initializer, itemVariable); + result += this.indent + this.transpileBinaryExpression(assignment) + ";\n"; + } this.popIndent(); } @@ -621,8 +635,6 @@ export abstract class LuaTranspiler { result += this.transpileLoopBody(node); this.popIndent(); - this.genVarCounter++; - return result + this.indent + "end\n"; } @@ -784,8 +796,7 @@ export abstract class LuaTranspiler { return this.transpileIdentifier(node as ts.Identifier); case ts.SyntaxKind.StringLiteral: case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - const text = this.escapeString((node as ts.StringLiteral).text); - return `"${text}"`; + return this.transpileStringLiteral(node as ts.StringLiteralLike); case ts.SyntaxKind.TemplateExpression: return this.transpileTemplateExpression(node as ts.TemplateExpression); case ts.SyntaxKind.NumericLiteral: @@ -1191,12 +1202,14 @@ export abstract class LuaTranspiler { // Check for calls on primitives to override let parameters = ""; + const isLuaIterator = tsHelper.isLuaIteratorCall(node, this.checker); const isTupleReturn = tsHelper.isTupleReturnCall(node, this.checker); const isTupleReturnForward = node.parent && ts.isReturnStatement(node.parent) && tsHelper.isInTupleReturnFunction(node, this.checker); const isInDestructingAssignment = tsHelper.isInDestructingAssignment(node); const returnValueIsUsed = node.parent && !ts.isExpressionStatement(node.parent); - const wrapResult = isTupleReturn && !isTupleReturnForward && !isInDestructingAssignment && returnValueIsUsed; + const wrapResult = isTupleReturn && !isTupleReturnForward && !isInDestructingAssignment && returnValueIsUsed + && !isLuaIterator; if (ts.isPropertyAccessExpression(node.expression)) { const result = this.transpilePropertyCall(node); @@ -1315,15 +1328,15 @@ export abstract class LuaTranspiler { || tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== ContextType.Void) { // Pass left-side as context - // Inject context parameter - if (node.arguments.length > 0) { - parameters = "____TS_self, " + parameters; - } else { - parameters = "____TS_self"; - } - const context = this.transpileExpression(node.expression.expression); if (tsHelper.isExpressionWithEvaluationEffect(node.expression.expression)) { + // Inject context parameter + if (node.arguments.length > 0) { + parameters = "____TS_self, " + parameters; + } else { + parameters = "____TS_self"; + } + // Cache left-side if it has effects const argument = this.transpileExpression(node.expression.argumentExpression); if (tsHelper.isExpressionStatement(node)) { @@ -1335,6 +1348,9 @@ export abstract class LuaTranspiler { + `return ____TS_self[${argument}](${parameters}); end)()`; } } else { + if (node.arguments.length > 0) { + parameters = ", " + parameters; + } return `${this.transpileExpression(node.expression)}(${context}${parameters})`; } } else { @@ -1631,9 +1647,16 @@ export abstract class LuaTranspiler { return property; } - // Catch math expressions - if (ts.isIdentifier(node.expression) && this.transpileIdentifier(node.expression) === "Math") { - return this.transpileMathExpression(node.name); + if (ts.isIdentifier(node.expression)) { + const name = this.transpileIdentifier(node.expression); + if (name === "Math") { + // Catch math expressions + return this.transpileMathExpression(node.name); + + } else if (name === "Symbol") { + // Pull in Symbol lib + this.importLuaLibFeature(LuaLibFeature.Symbol); + } } const callPath = this.transpileExpression(node.expression); @@ -1720,6 +1743,23 @@ export abstract class LuaTranspiler { } } + public transpileStringLiteral(literal: ts.StringLiteralLike): string { + const text = this.escapeString(literal.text); + return `"${text}"`; + } + + public transpilePropertyName(propertyName: ts.PropertyName): string { + if (ts.isComputedPropertyName(propertyName)) { + return this.transpileExpression(propertyName.expression); + } else if (ts.isStringLiteral(propertyName)) { + return this.transpileStringLiteral(propertyName); + } else if (ts.isNumericLiteral(propertyName)) { + return propertyName.text; + } else { + return this.transpileIdentifier(propertyName); + } + } + // Counter-act typescript's identifier escaping: // https://github.com/Microsoft/TypeScript/blob/master/src/compiler/utilities.ts#L556 public transpileIdentifier(identifier: ts.Identifier): string { @@ -1774,7 +1814,7 @@ export abstract class LuaTranspiler { node.declarationList.declarations.forEach(declaration => { result += this.transpileVariableDeclaration(declaration as ts.VariableDeclaration) + ";\n"; if (ts.isIdentifier(declaration.name)) { - this.pushExport(this.transpileIdentifier(declaration.name as ts.Identifier), node); + this.pushExport(this.transpileIdentifier(declaration.name), node); } }); @@ -1919,7 +1959,7 @@ export abstract class LuaTranspiler { if (!node.body) { return ""; } let result = ""; - let methodName = this.transpileIdentifier(node.name as ts.Identifier); + let methodName = this.transpilePropertyName(node.name); if (methodName === "toString") { methodName = "__tostring"; } @@ -1929,7 +1969,11 @@ export abstract class LuaTranspiler { const [paramNames, spreadIdentifier] = this.transpileParameters(node.parameters, context); // Build function header - result += this.indent + `function ${callPath}${methodName}(${paramNames.join(",")})\n`; + if (!ts.isIdentifier(node.name)) { + result += this.indent + `${callPath}[${methodName}] = function(${paramNames.join(",")})\n`; + } else { + result += this.indent + `function ${callPath}.${methodName}(${paramNames.join(",")})\n`; + } this.pushIndent(); result += this.transpileFunctionBody(node.parameters, node.body, spreadIdentifier); @@ -1997,20 +2041,25 @@ export abstract class LuaTranspiler { } else { for (const f of instanceFields) { // Get identifier - const fieldIdentifier = f.name as ts.Identifier; - const fieldName = this.transpileIdentifier(fieldIdentifier); - + const fieldName = this.transpilePropertyName(f.name); const value = this.transpileExpression(f.initializer); - - result += this.indent + `${className}.${fieldName} = ${value}\n`; + if (ts.isIdentifier(f.name)) { + result += this.indent + `${className}.${fieldName} = ${value}\n`; + } else { + result += this.indent + `${className}[${fieldName}] = ${value}\n`; + } } } // Add static declarations for (const field of staticFields) { - const fieldName = this.transpileIdentifier(field.name as ts.Identifier); + const fieldName = this.transpilePropertyName(field.name); const value = this.transpileExpression(field.initializer); - result += this.indent + `${className}.${fieldName} = ${value}\n`; + if (ts.isIdentifier(field.name)) { + result += this.indent + `${className}.${fieldName} = ${value}\n`; + } else { + result += this.indent + `${className}[${fieldName}] = ${value}\n`; + } } // Find first constructor with body @@ -2037,7 +2086,7 @@ export abstract class LuaTranspiler { // Transpile methods node.members.filter(ts.isMethodDeclaration).forEach(method => { - result += this.transpileMethodDeclaration(method, `${className}.`); + result += this.transpileMethodDeclaration(method, className); }); return result; @@ -2074,12 +2123,13 @@ export abstract class LuaTranspiler { for (const f of instanceFields) { // Get identifier - const fieldIdentifier = f.name as ts.Identifier; - const fieldName = this.transpileIdentifier(fieldIdentifier); - + const fieldName = this.transpilePropertyName(f.name); const value = this.transpileExpression(f.initializer); - - result += this.indent + ` self.${fieldName} = ${value}\n`; + if (ts.isIdentifier(f.name)) { + result += this.indent + ` self.${fieldName} = ${value}\n`; + } else { + result += this.indent + ` self[${fieldName}] = ${value}\n`; + } } result += this.indent + ` if construct and ${className}.constructor then ` @@ -2092,7 +2142,7 @@ export abstract class LuaTranspiler { } public transpileGetAccessorDeclaration(getAccessor: ts.GetAccessorDeclaration, className: string): string { - const name = this.transpileIdentifier(getAccessor.name as ts.Identifier); + const name = this.transpilePropertyName(getAccessor.name); let result = this.indent + `function ${className}.get__${name}(self)\n`; @@ -2106,7 +2156,7 @@ export abstract class LuaTranspiler { } public transpileSetAccessorDeclaration(setAccessor: ts.SetAccessorDeclaration, className: string): string { - const name = this.transpileIdentifier(setAccessor.name as ts.Identifier); + const name = this.transpilePropertyName(setAccessor.name); const paramNames: string[] = ["self"]; setAccessor.parameters.forEach(param => { diff --git a/src/lualib/Iterator.ts b/src/lualib/Iterator.ts new file mode 100644 index 000000000..805792052 --- /dev/null +++ b/src/lualib/Iterator.ts @@ -0,0 +1,11 @@ +function __TS__Iterator(iterable: Iterable): () => T { + const iterator = iterable[Symbol.iterator](); + return () => { + const result = iterator.next(); + if (!result.done) { + return result.value; + } else { + return undefined; + } + }; +} diff --git a/src/lualib/Map.ts b/src/lualib/Map.ts index cc6110d78..fd55ef62f 100644 --- a/src/lualib/Map.ts +++ b/src/lualib/Map.ts @@ -1,21 +1,34 @@ +/** @tupleReturn */ +declare function next(t: { [k: string]: TValue }, index?: TKey): [TKey, TValue]; + class Map { public size: number; private items: {[key: string]: TValue}; // Type of key is actually TKey - constructor(other: Map | Array<[TKey, TValue]>) { + constructor(other: Iterable<[TKey, TValue]> | Array<[TKey, TValue]>) { this.items = {}; this.size = 0; - if (other instanceof Map) { - this.size = other.size; - for (const kvp of other.entries()) { - this.items[kvp[0] as any] = kvp[1]; - } - } else if (other !== undefined) { - this.size = other.length; - for (const kvp of other) { - this.items[kvp[0] as any] = kvp[1]; + if (other) { + const iterable = other as Iterable<[TKey, TValue]>; + if (iterable[Symbol.iterator]) { + // Iterate manually because Map is compiled with ES5 which doesn't support Iterables in for...of + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) { + break; + } + const value: [TKey, TValue] = result.value; // Ensures index is offset when tuple is accessed + this.set(value[0], value[1]); + } + } else { + const arr = other as Array<[TKey, TValue]>; + this.size = arr.length; + for (const kvp of arr) { + this.items[kvp[0] as any] = kvp[1]; + } } } } @@ -35,12 +48,21 @@ class Map { return contains; } - public entries(): Array<[TKey, TValue]> { - const out = []; - for (const key in this.items) { - out[out.length] = [key, this.items[key]]; - } - return out; + public [Symbol.iterator](): IterableIterator<[TKey, TValue]> { + return this.entries(); + } + + public entries(): IterableIterator<[TKey, TValue]> { + const items = this.items; + let key: TKey; + let value: TValue; + return { + [Symbol.iterator](): IterableIterator<[TKey, TValue]> { return this; }, + next(): IteratorResult<[TKey, TValue]> { + [key, value] = next(items, key); + return {done: !key, value: [key, value]}; + }, + }; } public forEach(callback: (value: TValue, key: TKey, map: Map) => any): void { @@ -58,12 +80,16 @@ class Map { return this.items[key as any] !== undefined; } - public keys(): TKey[] { - const out = []; - for (const key in this.items) { - out[out.length] = key; - } - return out; + public keys(): IterableIterator { + const items = this.items; + let key: TKey; + return { + [Symbol.iterator](): IterableIterator { return this; }, + next(): IteratorResult { + [key] = next(items, key); + return {done: !key, value: key}; + }, + }; } public set(key: TKey, value: TValue): Map { @@ -74,11 +100,16 @@ class Map { return this; } - public values(): TValue[] { - const out = []; - for (const key in this.items) { - out[out.length] = this.items[key]; - } - return out; + public values(): IterableIterator { + const items = this.items; + let key: TKey; + let value: TValue; + return { + [Symbol.iterator](): IterableIterator { return this; }, + next(): IteratorResult { + [key, value] = next(items, key); + return {done: !key, value}; + }, + }; } } diff --git a/src/lualib/Set.ts b/src/lualib/Set.ts index 81021dbef..b81bc25c8 100644 --- a/src/lualib/Set.ts +++ b/src/lualib/Set.ts @@ -1,21 +1,33 @@ +/** @tupleReturn */ +declare function next(t: { [k: string]: TValue }, index?: TKey): [TKey, TValue]; + class Set { public size: number; private items: {[key: string]: boolean}; // Key type is actually TValue - constructor(other: Set | TValue[]) { + constructor(other: Iterable | TValue[]) { this.items = {}; this.size = 0; - if (other instanceof Set) { - this.size = other.size; - for (const value of other.values()) { - this.items[value as any] = true as any; - } - } else if (other !== undefined) { - this.size = other.length; - for (const value of other) { - this.items[value as any] = true as any; + if (other) { + const iterable = other as Iterable; + if (iterable[Symbol.iterator]) { + // Iterate manually because Set is compiled with ES5 which doesn't support Iterables in for...of + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) { + break; + } + this.add(result.value); + } + } else { + const arr = other as TValue[]; + this.size = arr.length; + for (const value of arr) { + this.items[value as any] = true as any; + } } } } @@ -43,12 +55,20 @@ class Set { return contains; } - public entries(): Array<[TValue, TValue]> { - const out = []; - for (const key in this.items) { - out[out.length] = [key, key]; - } - return out; + public [Symbol.iterator](): IterableIterator { + return this.values(); + } + + public entries(): IterableIterator<[TValue, TValue]> { + const items = this.items; + let key: TValue; + return { + [Symbol.iterator](): IterableIterator<[TValue, TValue]> { return this; }, + next(): IteratorResult<[TValue, TValue]> { + [key] = next(items, key); + return {done: !key, value: [key, key]}; + }, + }; } public forEach(callback: (value: TValue, key: TValue, set: Set) => any): void { @@ -62,19 +82,27 @@ class Set { return this.items[value as any] === true; } - public keys(): TValue[] { - const out = []; - for (const key in this.items) { - out[out.length] = key; - } - return out; + public keys(): IterableIterator { + const items = this.items; + let key: TValue; + return { + [Symbol.iterator](): IterableIterator { return this; }, + next(): IteratorResult { + [key] = next(items, key); + return {done: !key, value: key}; + }, + }; } - public values(): TValue[] { - const out = []; - for (const key in this.items) { - out[out.length] = key; - } - return out; + public values(): IterableIterator { + const items = this.items; + let key: TValue; + return { + [Symbol.iterator](): IterableIterator { return this; }, + next(): IteratorResult { + [key] = next(items, key); + return {done: !key, value: key}; + }, + }; } } diff --git a/src/lualib/Symbol.ts b/src/lualib/Symbol.ts new file mode 100644 index 000000000..478fd49c8 --- /dev/null +++ b/src/lualib/Symbol.ts @@ -0,0 +1,3 @@ +Symbol = { + iterator: {}, +} as any; diff --git a/src/tstl.ts b/src/tstl.ts index 31174588c..37d413fc5 100644 --- a/src/tstl.ts +++ b/src/tstl.ts @@ -19,12 +19,15 @@ export {LuaTranspiler53} from "./targets/Transpiler.53"; export {LuaTranspilerJIT} from "./targets/Transpiler.JIT"; export { - LuaLibFeature, LuaLibImportKind, LuaTarget, LuaTranspiler, } from "./Transpiler"; +export { + LuaLibFeature, +} from "./LuaLib"; + export { createTranspiler } from "./TranspilerFactory"; diff --git a/test/translation/lua/forOf.lua b/test/translation/lua/forOf.lua index 4a7ca76a9..1d3fecdb8 100644 --- a/test/translation/lua/forOf.lua +++ b/test/translation/lua/forOf.lua @@ -1,6 +1,6 @@ -local __loopVariable0 = {1,2,3,4,5,6,7,8,9,10}; -for i0=1, #__loopVariable0 do - local i = __loopVariable0[i0]; +local ____TS_array = {1,2,3,4,5,6,7,8,9,10}; +for ____TS_index=1, #____TS_array do + local i = ____TS_array[____TS_index]; do end ::__continue0:: diff --git a/test/unit/class.spec.ts b/test/unit/class.spec.ts index 4c7c3b9a5..81851ab9e 100644 --- a/test/unit/class.spec.ts +++ b/test/unit/class.spec.ts @@ -22,6 +22,58 @@ export class ClassTests { Expect(result).toBe(4); } + @Test("ClassNumericLiteralFieldInitializer") + public classNumericLiteralFieldInitializer(): void { + // Transpile + const lua = util.transpileString( + `class a { + 1: number = 4; + } + return new a()[1];` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + + @Test("ClassStringLiteralFieldInitializer") + public classStringLiteralFieldInitializer(): void { + // Transpile + const lua = util.transpileString( + `class a { + "field": number = 4; + } + return new a()["field"];` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + + @Test("ClassComputedFieldInitializer") + public classComputedFieldInitializer(): void { + // Transpile + const lua = util.transpileString( + `const field: "field" = "field"; + class a { + [field]: number = 4; + } + return new a()[field];` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + @Test("ClassConstructor") public classConstructor(): void { // Transpile @@ -111,6 +163,52 @@ export class ClassTests { Expect(result).toBe(4); } + @Test("ClassStaticNumericLiteralFields") + public classStaticNumericLiteralFields(): void { + // Transpile + const lua = util.transpileString( + `class a { static 1: number = 4; } + return a[1];` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + + @Test("ClassStaticStringLiteralFields") + public classStaticStringLiteralFields(): void { + // Transpile + const lua = util.transpileString( + `class a { static "field": number = 4; } + return a["field"];` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + + @Test("ClassStaticComputedFields") + public classStaticComputedFields(): void { + // Transpile + const lua = util.transpileString( + `const field: "field" = "field"; + class a { static [field]: number = 4; } + return a[field];` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + @Test("classExtends") public classExtends(): void { // Transpile @@ -255,6 +353,67 @@ export class ClassTests { Expect(result).toBe(4); } + @Test("ClassNumericLiteralMethodCall") + public classNumericLiteralMethodCall(): void { + // Transpile + const lua = util.transpileString( + `class a { + public 1(): number { + return 4; + } + } + let inst = new a(); + return inst[1]();` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + + @Test("ClassStringLiteralMethodCall") + public classStringLiteralMethodCall(): void { + // Transpile + const lua = util.transpileString( + `class a { + public "method"(): number { + return 4; + } + } + let inst = new a(); + return inst["method"]();` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + + @Test("ClassComputedMethodCall") + public classComputedMethodCall(): void { + // Transpile + const lua = util.transpileString( + `const method: "method" = "method"; + class a { + public [method](): number { + return 4; + } + } + let inst = new a(); + return inst[method]();` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(4); + } + @Test("ClassToString") public classToString(): void { // Transpile diff --git a/test/unit/enum.spec.ts b/test/unit/enum.spec.ts index ee7e5548e..5ca2303dc 100644 --- a/test/unit/enum.spec.ts +++ b/test/unit/enum.spec.ts @@ -89,4 +89,14 @@ export class EnumTests { ); }).toThrowError(TranspileError, "Only numeric or string initializers allowed for enums."); } + + @Test("String literal name in enum") + public stringLiteralNameEnum(): void { + const code = `enum TestEnum { + ["name"] = "foo" + } + return TestEnum["name"];`; + const result = util.transpileAndExecute(code); + Expect(result).toBe("foo"); + } } diff --git a/test/unit/functions.spec.ts b/test/unit/functions.spec.ts index 05fe4e97d..a619e7ce0 100644 --- a/test/unit/functions.spec.ts +++ b/test/unit/functions.spec.ts @@ -385,38 +385,41 @@ export class FunctionTests { @Test("Element access call") public elementAccessCall(): void { const code = `class C { - method(s: string) { return s; } + prop = "bar"; + method(s: string) { return s + this.prop; } } const c = new C(); return c['method']("foo"); `; const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); + Expect(result).toBe("foobar"); } @Test("Complex element access call") public elementAccessCallComplex(): void { const code = `class C { - method(s: string) { return s; } + prop = "bar"; + method(s: string) { return s + this.prop; } } function getC() { return new C(); } return getC()['method']("foo"); `; const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); + Expect(result).toBe("foobar"); } @Test("Complex element access call statement") public elementAccessCallComplexStatement(): void { const code = `let foo: string; class C { - method(s: string) { foo = s; } + prop = "bar"; + method(s: string) { foo = s + this.prop; } } function getC() { return new C(); } getC()['method']("foo"); return foo; `; const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); + Expect(result).toBe("foobar"); } } diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index 3340ad8b7..a347ce932 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -1,6 +1,7 @@ import { Expect, Test, TestCase } from "alsatian"; +import * as ts from "typescript"; import { TranspileError } from "../../src/Errors"; -import { LuaTarget } from "../../src/Transpiler"; +import { LuaLibImportKind, LuaTarget } from "../../src/Transpiler"; import * as util from "../src/util"; const deepEqual = require("deep-equal"); @@ -356,6 +357,27 @@ export class LuaLoopTests { Expect(result).toBe(JSON.stringify(expected)); } + @TestCase([0, 1, 2], [1, 2, 3]) + @Test("forof existing variable") + public forofExistingVar(inp: any, expected: any): void { + // Transpile + const lua = util.transpileString( + `let objTest = ${JSON.stringify(inp)}; + let arrResultTest = []; + let value: number; + for (value of objTest) { + arrResultTest.push(value + 1) + } + return JSONStringify(arrResultTest);` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(JSON.stringify(expected)); + } + @TestCase([[1, 2], [2, 3], [3, 4]], [3, 5, 7]) @Test("forof destructing") public forofDestructing(inp: number[][], expected: any): void { @@ -376,6 +398,28 @@ export class LuaLoopTests { Expect(result).toBe(JSON.stringify(expected)); } + @TestCase([[1, 2], [2, 3], [3, 4]], [3, 5, 7]) + @Test("forof destructing with existing variables") + public forofDestructingExistingVars(inp: number[][], expected: any): void { + // Transpile + const lua = util.transpileString( + `let objTest = ${JSON.stringify(inp)}; + let arrResultTest = []; + let a: number; + let b: number; + for ([a,b] of objTest) { + arrResultTest.push(a + b) + } + return JSONStringify(arrResultTest);` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + Expect(result).toBe(JSON.stringify(expected)); + } + @TestCase([0, 1, 2, 3, 4], [0, 0, 2, 0, 4]) @Test("forof with continue") public forofWithContinue(inp: number[], expected: number[]): void { @@ -407,6 +451,283 @@ export class LuaLoopTests { Expect(result).toBe(JSON.stringify(expected)); } + @Test("forof with iterator") + public forofWithIterator(): void { + const code = `const arr = ["a", "b", "c"]; + function iter(): IterableIterator { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: arr[i], done: i++ >= arr.length} }, + } + } + let result = ""; + for (let e of iter()) { + result += e; + } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("abc"); + } + + @Test("forof with iterator and existing variable") + public forofWithIteratorExistingVar(): void { + const code = `const arr = ["a", "b", "c"]; + function iter(): IterableIterator { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: arr[i], done: i++ >= arr.length} }, + } + } + let result = ""; + let e: string; + for (e of iter()) { + result += e; + } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("abc"); + } + + @Test("forof destructuring with iterator") + public forofDestructuringWithIterator(): void { + const code = `const arr = ["a", "b", "c"]; + function iter(): IterableIterator<[string, string]> { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: [i.toString(), arr[i]], done: i++ >= arr.length} }, + } + } + let result = ""; + for (let [a, b] of iter()) { + result += a + b; + } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("0a1b2c"); + } + + @Test("forof destructuring with iterator and existing variables") + public forofDestructuringWithIteratorExistingVars(): void { + const code = `const arr = ["a", "b", "c"]; + function iter(): IterableIterator<[string, string]> { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: [i.toString(), arr[i]], done: i++ >= arr.length} }, + } + } + let result = ""; + let a: string; + let b: string; + for ([a, b] of iter()) { + result += a + b; + } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("0a1b2c"); + } + + @Test("forof lua iterator") + public forofLuaIterator(): void { + const code = `const arr = ["a", "b", "c"]; + /** @luaIterator */ + function luaIter(): Iterable { + let i = 0; + return (() => arr[i++]) as any; + } + let result = ""; + for (let e of luaIter()) { result += e; } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("abc"); + } + + @Test("forof lua iterator with existing variable") + public forofLuaIteratorExistingVar(): void { + const code = `const arr = ["a", "b", "c"]; + /** @luaIterator */ + function luaIter(): Iterable { + let i = 0; + return (() => arr[i++]) as any; + } + let result = ""; + let e: string; + for (e of luaIter()) { result += e; } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("abc"); + } + + @Test("forof lua iterator destructuring") + public forofLuaIteratorDestructuring(): void { + const code = `const arr = ["a", "b", "c"]; + /** @luaIterator */ + function luaIter(): Iterable<[string, string]> { + let i = 0; + return (() => arr[i] && [i.toString(), arr[i++]]) as any; + } + let result = ""; + for (let [a, b] of luaIter()) { result += a + b; } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("0a1b2c"); + } + + @Test("forof lua iterator destructuring with existing variables") + public forofLuaIteratorDestructuringExistingVar(): void { + const code = `const arr = ["a", "b", "c"]; + /** @luaIterator */ + function luaIter(): Iterable<[string, string]> { + let i = 0; + return (() => arr[i] && [i.toString(), arr[i++]]) as any; + } + let result = ""; + let a: string; + let b: string; + for ([a, b] of luaIter()) { result += a + b; } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("0a1b2c"); + } + + @Test("forof lua iterator tuple-return") + public forofLuaIteratorTupleReturn(): void { + const code = `const arr = ["a", "b", "c"]; + /** @luaIterator */ + /** @tupleReturn */ + function luaIter(): Iterable<[string, string]> { + let i = 0; + /** @tupleReturn */ + function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } + return iter as any; + } + let result = ""; + for (let [a, b] of luaIter()) { result += a + b; } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("0a1b2c"); + } + + @Test("forof lua iterator tuple-return with existing variables") + public forofLuaIteratorTupleReturnExistingVars(): void { + const code = `const arr = ["a", "b", "c"]; + /** @luaIterator */ + /** @tupleReturn */ + function luaIter(): Iterable<[string, string]> { + let i = 0; + /** @tupleReturn */ + function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } + return iter as any; + } + let result = ""; + let a: string; + let b: string; + for ([a, b] of luaIter()) { result += a + b; } + return result;`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const lua = util.transpileString(code, compilerOptions); + const result = util.executeLua(lua); + Expect(result).toBe("0a1b2c"); + } + + @Test("forof lua iterator tuple-return single variable") + public forofLuaIteratorTupleReturnSingleVar(): void { + const code = `/** @luaIterator */ + /** @tupleReturn */ + declare function luaIter(): Iterable<[string, string]>; + for (let x of luaIter()) {}`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + Expect(() => util.transpileString(code, compilerOptions)).toThrowError( + TranspileError, + "Unsupported use of lua iterator with TupleReturn decorator in for...of statement. " + + "You must use a destructuring statement to catch results from a lua iterator with " + + "the TupleReturn decorator."); + } + + @Test("forof lua iterator tuple-return single existing variable") + public forofLuaIteratorTupleReturnSingleExistingVar(): void { + const code = `/** @luaIterator */ + /** @tupleReturn */ + declare function luaIter(): Iterable<[string, string]>; + let x: [string, string]; + for (x of luaIter()) {}`; + const compilerOptions = { + luaLibImport: LuaLibImportKind.Require, + luaTarget: LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + Expect(() => util.transpileString(code, compilerOptions)).toThrowError( + TranspileError, + "Unsupported use of lua iterator with TupleReturn decorator in for...of statement. " + + "You must use a destructuring statement to catch results from a lua iterator with " + + "the TupleReturn decorator."); + } + @TestCase("while (a < b) { i++; }") @TestCase("do { i++; } while (a < b)") @TestCase("for (let i = 0; i < 3; i++) {}")