diff --git a/.gitignore b/.gitignore index c650dfcea..c0af5a22a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.js node_modules/ *.lua +!json.lua !dist/*.js !dist/lualib/*.lua diff --git a/dist/Transpiler.js b/dist/Transpiler.js index 5b0821b53..38da095a0 100644 --- a/dist/Transpiler.js +++ b/dist/Transpiler.js @@ -246,11 +246,11 @@ var LuaTranspiler = /** @class */ (function () { var identifier = variable.name; // Transpile expression var expression = this.transpileExpression(node.expression); - // Use ipairs for array types, pairs otherwise - var isArray = TSHelper_1.TSHelper.isArrayType(this.checker.getTypeAtLocation(node.expression)); - var pairs = isArray ? "ipairs" : "pairs"; + if (TSHelper_1.TSHelper.isArrayType(this.checker.getTypeAtLocation(node.expression))) { + throw new TranspileError("Iterating over arrays with 'for in' is not allowed.", node); + } // Make header - var result = this.indent + ("for " + identifier.escapedText + ", _ in " + pairs + "(" + expression + ") do\n"); + var result = this.indent + ("for " + identifier.escapedText + ", _ in pairs(" + expression + ") do\n"); // For body this.pushIndent(); result += this.transpileStatement(node.statement); @@ -561,6 +561,8 @@ var LuaTranspiler = /** @class */ (function () { return "TS_slice(" + caller + ", " + params + ")"; case "splice": return "TS_splice(" + caller + ", " + params + ")"; + case "join": + return "table.concat(" + caller + ", " + params + ")"; default: throw new TranspileError("Unsupported array function: " + expression.name.escapedText, node); } diff --git a/package-lock.json b/package-lock.json index 1b2c9b607..fa9c8b183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,12 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", diff --git a/package.json b/package.json index fc36866f9..4638cea4b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "coverage": "nyc --reporter=lcov npm test && nyc report" }, "devDependencies": { + "deep-equal": "^1.0.1", "nyc": "^11.4.1" } } diff --git a/src/Transpiler.ts b/src/Transpiler.ts index 8397c64a4..e834ff27d 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -274,7 +274,7 @@ export class LuaTranspiler { } // Make header - let result = this.indent + `for ${identifier.escapedText}, _ in "pairs"(${expression}) do\n`; + let result = this.indent + `for ${identifier.escapedText}, _ in pairs(${expression}) do\n`; // For body this.pushIndent(); diff --git a/test/integration/lua/loops.spec.ts b/test/integration/lua/loops.spec.ts new file mode 100644 index 000000000..4da4ab365 --- /dev/null +++ b/test/integration/lua/loops.spec.ts @@ -0,0 +1,83 @@ +import { Expect, Test, TestCase } from "alsatian"; +import * as util from "../../src/util" + +const deepEqual = require('deep-equal') + +export class LuaLoopTests { + + @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) + @Test("for") + public for(inp: T[], expected: T[]) { + // Transpile + let lua = util.transpileString( + `let arrTest = ${JSON.stringify(inp)}; + for (let i = 0; i < arrTest.length; ++i) { + arrTest[i] = arrTest[i] + 1; + } + return JSONStringify(arrTest);` + , util.dummyTypes.Array + ); + + // Execute + let result = util.executeLua(lua); + + // Assert + Expect(result).toBe(JSON.stringify(expected)); + } + + @TestCase({ ['test1']: 0, ['test2']: 1, ['test3']: 2 }, { ['test1']: 1, ['test2']: 2, ['test3']: 3 }) + @Test("forin[Object]") + public forinObject(inp: any, expected: any) { + // Transpile + let lua = util.transpileString( + `let objTest = ${JSON.stringify(inp)}; + for (let key in objTest) { + objTest[key] = objTest[key] + 1; + } + return JSONStringify(objTest);` + , util.dummyTypes.Object + ); + + // Execute + let result = util.executeLua(lua); + + // Assert + Expect(deepEqual(JSON.parse(result), expected)).toBe(true); + } + + @TestCase([1,2,3]) + @Test("forin[Array]") + public forinArray(inp: T[]) { + // Transpile & Assert + Expect(() => { + let lua = util.transpileString( + `let arrTest = ${JSON.stringify(inp)}; + for (let key in arrTest) { + arrTest[key]++; + }` + , util.dummyTypes.Array + ); + }).toThrowError(Error, "Iterating over arrays with 'for in' is not allowed."); + } + + @TestCase([0,1,2], [1,2,3]) + @Test("forof") + public forof(inp: any, expected: any) { + // Transpile + let lua = util.transpileString( + `let objTest = ${JSON.stringify(inp)}; + let arrResultTest = {}; + for (let value of objTest) { + arrResultTest.push(value + 1) + } + return JSONStringify(arrResultTest);` + , util.dummyTypes.Array + ); + + // Execute + let result = util.executeLua(lua); + + // Assert + Expect(result).toBe(JSON.stringify(expected)); + } +} diff --git a/test/integration/lua/lualib.spec.ts b/test/integration/lua/lualib.spec.ts index 56331e012..57063709d 100644 --- a/test/integration/lua/lualib.spec.ts +++ b/test/integration/lua/lualib.spec.ts @@ -1,201 +1,141 @@ import { Expect, Test, TestCase } from "alsatian"; +import * as util from "../../src/util" -import * as ts from "typescript"; -import {LuaTranspiler, TranspileError} from "../../../dist/Transpiler"; - -const LuaVM = require("lua.vm.js"); -const fs = require("fs"); - -const dummyArrayType = { flags: ts.TypeFlags.Object, symbol: {escapedName: "Array"}}; -let dummyType = {}; -const dummyChecker = {getTypeAtLocation: function() {return dummyType;}} -function transpileString(str: string): string { - const file = ts.createSourceFile("", str, ts.ScriptTarget.Latest); - const result = LuaTranspiler.transpileSourceFile(file, dummyChecker, false); - return result.trim(); -} -function executeLua(lua: string): string { - const luavm = new LuaVM.Lua.State(); - return luavm.execute(lua)[0]; -} - -const lualib = fs.readFileSync("dist/lualib/typescript.lua") + "\n"; - -const toStringDef = "function ToString(list)\n"+ - "local result = \"\"\n" + - "for i=1,#list do result = result .. list[i]\n" + - "if i < #list then result = result .. ',' end end\n"+ - "return result end\n"; - -export class LuaTests { +export class LuaLibArrayTests { @TestCase([], "x => x") - @TestCase([0,1,2,3], "x => x") - @TestCase([0,1,2,3], "x => x*2") - @TestCase([1,2,3,4], "x => -x") - @TestCase([0,1,2,3], "x => x+2") - @TestCase([0,1,2,3], "x => x%2 == 0 ? x + 1 : x - 1") + @TestCase([0, 1, 2, 3], "x => x") + @TestCase([0, 1, 2, 3], "x => x*2") + @TestCase([1, 2, 3, 4], "x => -x") + @TestCase([0, 1, 2, 3], "x => x+2") + @TestCase([0, 1, 2, 3], "x => x%2 == 0 ? x + 1 : x - 1") @Test("array.map") public map(inp: T[], func: string) { - // Make typechecker return array type - dummyType = dummyArrayType; // Transpile - let lua = transpileString(`return ToString([${inp.toString()}].map(${func}))`); - - // Add library - lua = toStringDef + lualib + lua; + let lua = util.transpileString(`return JSONStringify([${inp.toString()}].map(${func}))`, util.dummyTypes.Array); // Execute - let result = executeLua(lua); + let result = util.executeLua(lua); // Assert - Expect(result).toBe(inp.map(eval(func)).toString()); + Expect(result).toBe(JSON.stringify(inp.map(eval(func)))); } @TestCase([], "x => x > 1") - @TestCase([0,1,2,3], "x => x > 1") - @TestCase([0,1,2,3], "x => x < 3") - @TestCase([0,1,2,3], "x => x < 0") - @TestCase([0,-1,-2,-3], "x => x < 0") - @TestCase([0,1,2,3], "() => true") - @TestCase([0,1,2,3], "() => false") + @TestCase([0, 1, 2, 3], "x => x > 1") + @TestCase([0, 1, 2, 3], "x => x < 3") + @TestCase([0, 1, 2, 3], "x => x < 0") + @TestCase([0, -1, -2, -3], "x => x < 0") + @TestCase([0, 1, 2, 3], "() => true") + @TestCase([0, 1, 2, 3], "() => false") @Test("array.filter") public filter(inp: T[], func: string) { - // Make typechecker return array type - dummyType = dummyArrayType; // Transpile - let lua = transpileString(`return ToString([${inp.toString()}].filter(${func}))`); - - // Add library - lua = toStringDef + lualib + lua; + let lua = util.transpileString(`return JSONStringify([${inp.toString()}].filter(${func}))`, util.dummyTypes.Array); // Execute - let result = executeLua(lua); + let result = util.executeLua(lua); // Assert - Expect(result).toBe(inp.filter(eval(func)).toString()); + Expect(result).toBe(JSON.stringify(inp.filter(eval(func)))); } @TestCase([], "x => x > 1") - @TestCase([0,1,2,3], "x => x > 1") + @TestCase([0, 1, 2, 3], "x => x > 1") @TestCase([false, true, false], "x => x") @TestCase([true, true, true], "x => x") @Test("array.every") public every(inp: T[], func: string) { - // Make typechecker return array type - dummyType = dummyArrayType; // Transpile - let lua = transpileString(`return [${inp.toString()}].every(${func}))`); - - // Add library - lua = toStringDef + lualib + lua; + let lua = util.transpileString(`return [${inp.toString()}].every(${func}))`, util.dummyTypes.Array); // Execute - let result = executeLua(lua); + let result = util.executeLua(lua); // Assert - Expect(result.toString()).toBe(inp.every(eval(func)).toString()); + Expect(JSON.stringify(result)).toBe(JSON.stringify(inp.every(eval(func)))); } @TestCase([], "x => x > 1") - @TestCase([0,1,2,3], "x => x > 1") + @TestCase([0, 1, 2, 3], "x => x > 1") @TestCase([false, true, false], "x => x") @TestCase([true, true, true], "x => x") @Test("array.some") public some(inp: T[], func: string) { - // Make typechecker return array type - dummyType = dummyArrayType; // Transpile - let lua = transpileString(`return [${inp.toString()}].some(${func}))`); - - // Add library - lua = toStringDef + lualib + lua; + let lua = util.transpileString(`return [${inp.toString()}].some(${func}))`, util.dummyTypes.Array); // Execute - let result = executeLua(lua); + let result = util.executeLua(lua); // Assert - Expect(result.toString()).toBe(inp.some(eval(func)).toString()); + Expect(JSON.stringify(result)).toBe(JSON.stringify(inp.some(eval(func)))); } @TestCase([], 1, 2) - @TestCase([0,1,2,3], 1, 2) - @TestCase([0,1,2,3], 1, 1) - @TestCase([0,1,2,3], 1, -1) - @TestCase([0,1,2,3], -3, -1) - @TestCase([0,1,2,3,4,5], 1, 3) - @TestCase([0,1,2,3,4,5], 3) + @TestCase([0, 1, 2, 3], 1, 2) + @TestCase([0, 1, 2, 3], 1, 1) + @TestCase([0, 1, 2, 3], 1, -1) + @TestCase([0, 1, 2, 3], -3, -1) + @TestCase([0, 1, 2, 3, 4, 5], 1, 3) + @TestCase([0, 1, 2, 3, 4, 5], 3) @Test("array.slice") public slice(inp: T[], start: number, end?: number) { - // Make typechecker return array type - dummyType = dummyArrayType; // Transpile - let lua = transpileString(`return ToString([${inp.toString()}].slice(${start}, ${end}))`); - - // Add library - lua = toStringDef + lualib + lua; + let lua = util.transpileString(`return JSONStringify([${inp.toString()}].slice(${start}, ${end}))`, util.dummyTypes.Array); // Execute - let result = executeLua(lua); + let result = util.executeLua(lua); // Assert - Expect(result).toBe(inp.slice(start, end).toString()); + Expect(result).toBe(JSON.stringify(inp.slice(start, end))); } @TestCase([], 0, 0, 9, 10, 11) - @TestCase([0,1,2,3], 1, 0, 9, 10, 11) - @TestCase([0,1,2,3], 2, 2, 9, 10, 11) - @TestCase([0,1,2,3], 4, 1, 8, 9) - @TestCase([0,1,2,3], 4, 0, 8, 9) - @TestCase([0,1,2,3,4,5], 5, 9, 10, 11) - @TestCase([0,1,2,3,4,5], 3, 2, 3, 4, 5) + @TestCase([0, 1, 2, 3], 1, 0, 9, 10, 11) + @TestCase([0, 1, 2, 3], 2, 2, 9, 10, 11) + @TestCase([0, 1, 2, 3], 4, 1, 8, 9) + @TestCase([0, 1, 2, 3], 4, 0, 8, 9) + @TestCase([0, 1, 2, 3, 4, 5], 5, 9, 10, 11) + @TestCase([0, 1, 2, 3, 4, 5], 3, 2, 3, 4, 5) @Test("array.splice[Insert]") public spliceInsert(inp: T[], start: number, deleteCount: number, ...newElements: any[]) { - // Make typechecker return array type - dummyType = dummyArrayType; // Transpile - let lua = transpileString( - `let spliceTestTable = [${inp.toString()}] + let lua = util.transpileString( + `let spliceTestTable = [${inp.toString()}]; spliceTestTable.splice(${start}, ${deleteCount}, ${newElements}); - return ToString(spliceTestTable);` + return JSONStringify(spliceTestTable);`, + util.dummyTypes.Array ); - // Add library - lua = toStringDef + lualib + lua; - // Execute - let result = executeLua(lua); + let result = util.executeLua(lua); // Assert inp.splice(start, deleteCount, ...newElements) - Expect(result).toBe(inp.toString()); + Expect(result).toBe(JSON.stringify(inp)); } @TestCase([], 1, 1) - @TestCase([0,1,2,3], 1, 1) - @TestCase([0,1,2,3], 10, 1) - @TestCase([0,1,2,3], 4) - @TestCase([0,1,2,3,4,5], 3) - @TestCase([0,1,2,3,4,5], 2, 2) - @TestCase([0,1,2,3,4,5,6,7,8], 5, 9, 10, 11) + @TestCase([0, 1, 2, 3], 1, 1) + @TestCase([0, 1, 2, 3], 10, 1) + @TestCase([0, 1, 2, 3], 4) + @TestCase([0, 1, 2, 3, 4, 5], 3) + @TestCase([0, 1, 2, 3, 4, 5], 2, 2) + @TestCase([0, 1, 2, 3, 4, 5, 6, 7, 8], 5, 9, 10, 11) @Test("array.splice[Remove]") public spliceRemove(inp: T[], start: number, deleteCount?: number, ...newElements: any[]) { - // Make typechecker return array type - dummyType = dummyArrayType; // Transpile - let lua = transpileString(`return ToString([${inp.toString()}].splice(${start}, ${deleteCount}, ${newElements}))`); - - // Add library - lua = toStringDef + lualib + lua; + let lua = util.transpileString(`return JSONStringify([${inp.toString()}].splice(${start}, ${deleteCount}, ${newElements}))`, util.dummyTypes.Array); // Execute - let result = executeLua(lua); + let result = util.executeLua(lua); // Assert if (deleteCount) { - Expect(result).toBe(inp.splice(start, deleteCount, ...newElements).toString()); + Expect(result).toBe(JSON.stringify(inp.splice(start, deleteCount, ...newElements))); } else { - Expect(result).toBe(inp.splice(start).toString()); + Expect(result).toBe(JSON.stringify(inp.splice(start))); } } } diff --git a/test/runner.ts b/test/runner.ts index 07d2db0c9..b5f89b0f3 100644 --- a/test/runner.ts +++ b/test/runner.ts @@ -23,4 +23,4 @@ testRunner.run(testSet); // this will be called after all tests have been run //.then((results) => done()) // this will be called if there was a problem - //.catch((error) => doSomethingWith(error)); \ No newline at end of file + //.catch((error) => doSomethingWith(error)); diff --git a/test/src/json.lua b/test/src/json.lua new file mode 100644 index 000000000..6dafe9fe4 --- /dev/null +++ b/test/src/json.lua @@ -0,0 +1,144 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- +-- +-- LICENSE CONTENTS: +-- +-- Copyright (c) 2015 rxi +-- +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function JSONStringify(val) + return ( encode(val) ) +end diff --git a/test/src/util.ts b/test/src/util.ts new file mode 100644 index 000000000..009ebe1c4 --- /dev/null +++ b/test/src/util.ts @@ -0,0 +1,31 @@ +import * as ts from "typescript"; +import { LuaTranspiler, TranspileError } from "../../dist/Transpiler"; + +const LuaVM = require("lua.vm.js"); +const fs = require("fs"); + +export namespace dummyTypes { + export const Array = { flags: ts.TypeFlags.Object, symbol: { escapedName: "Array" } }; + export const Object = { flags: ts.TypeFlags.Object, symbol: { escapedName: "Object" } } +} + +export function transpileString(str: string, dummyType: any): string { + const dummyChecker = { getTypeAtLocation: function() { return dummyType; } } + const file = ts.createSourceFile("", str, ts.ScriptTarget.Latest); + const result = LuaTranspiler.transpileSourceFile(file, dummyChecker, false); + return result.trim(); +} + +export function executeLua(lua: string, withLib = true): string { + if (withLib) { + lua = minimalTestLib + lua + } + const luavm = new LuaVM.Lua.State(); + return luavm.execute(lua)[0]; +} + +const lualib = fs.readFileSync("dist/lualib/typescript.lua") + "\n"; + +const jsonlib = fs.readFileSync("test/src/json.lua") + "\n"; + +export const minimalTestLib = lualib + jsonlib