diff --git a/CHANGELOG.md b/CHANGELOG.md index eceb69bb4..dfb0e91a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Added new `"luaTarget"` option value - `"universal"`. Choosing this target would make TypeScriptToLua generate code compatible with all supported Lua targets. + - **BREAKING CHANGE:** This is a new default target. If you have been depending on LuaJIT being chosen implicitly, now you have to enable it explicitly with `"luaTarget": "JIT"` in the `tsconfig.json` file. + - TypeScript has been updated to 3.9. See [release notes](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/) for details. This update includes some fixes specific to our API usage: diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index af9a82d7d..45786a555 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -44,6 +44,7 @@ export enum LuaLibImportKind { } export enum LuaTarget { + Universal = "universal", Lua51 = "5.1", Lua52 = "5.2", Lua53 = "5.3", diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 33437a7b8..ea8962452 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -38,6 +38,7 @@ export enum LuaLibFeature { InstanceOfObject = "InstanceOfObject", Iterator = "Iterator", Map = "Map", + MathAtan2 = "MathAtan2", New = "New", Number = "Number", NumberIsFinite = "NumberIsFinite", @@ -67,12 +68,16 @@ export enum LuaLibFeature { Symbol = "Symbol", SymbolRegistry = "SymbolRegistry", TypeOf = "TypeOf", + Unpack = "Unpack", } const luaLibDependencies: Partial> = { ArrayFlat: [LuaLibFeature.ArrayConcat], ArrayFlatMap: [LuaLibFeature.ArrayConcat], Error: [LuaLibFeature.New, LuaLibFeature.Class, LuaLibFeature.FunctionCall], + FunctionApply: [LuaLibFeature.Unpack], + FunctionBind: [LuaLibFeature.Unpack], + FunctionCall: [LuaLibFeature.Unpack], Generator: [LuaLibFeature.Symbol], InstanceOf: [LuaLibFeature.Symbol], Iterator: [LuaLibFeature.Symbol], @@ -81,7 +86,7 @@ const luaLibDependencies: Partial> = { Set: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], WeakMap: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], WeakSet: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], - Spread: [LuaLibFeature.Iterator], + Spread: [LuaLibFeature.Iterator, LuaLibFeature.Unpack], SymbolRegistry: [LuaLibFeature.Symbol], }; diff --git a/src/lualib/FunctionApply.ts b/src/lualib/FunctionApply.ts index 294efdda9..2638a43a8 100644 --- a/src/lualib/FunctionApply.ts +++ b/src/lualib/FunctionApply.ts @@ -1,6 +1,6 @@ function __TS__FunctionApply(this: void, fn: (this: void, ...args: any[]) => any, thisArg: any, args?: any[]): any { if (args) { - return fn(thisArg, (unpack || table.unpack)(args)); + return fn(thisArg, ...args); } else { return fn(thisArg); } diff --git a/src/lualib/FunctionBind.ts b/src/lualib/FunctionBind.ts index 671570ab9..e12fbe37c 100644 --- a/src/lualib/FunctionBind.ts +++ b/src/lualib/FunctionBind.ts @@ -8,6 +8,6 @@ function __TS__FunctionBind( for (let i = 0; i < boundArgs.length; ++i) { table.insert(args, i + 1, boundArgs[i]); } - return fn(thisArg, (unpack || table.unpack)(args)); + return fn(thisArg, ...args); }; } diff --git a/src/lualib/FunctionCall.ts b/src/lualib/FunctionCall.ts index e9b50fbbe..8435e7239 100644 --- a/src/lualib/FunctionCall.ts +++ b/src/lualib/FunctionCall.ts @@ -1,3 +1,3 @@ function __TS__FunctionCall(this: void, fn: (this: void, ...args: any[]) => any, thisArg: any, ...args: any[]): any { - return fn(thisArg, (unpack || table.unpack)(args)); + return fn(thisArg, ...args); } diff --git a/src/lualib/MathAtan2.ts b/src/lualib/MathAtan2.ts new file mode 100644 index 000000000..fdf322664 --- /dev/null +++ b/src/lualib/MathAtan2.ts @@ -0,0 +1 @@ +const __TS__MathAtan2 = math.atan2 || math.atan; diff --git a/src/lualib/Spread.ts b/src/lualib/Spread.ts index d9dcf0345..2bc96395b 100644 --- a/src/lualib/Spread.ts +++ b/src/lualib/Spread.ts @@ -10,5 +10,5 @@ function __TS__Spread(this: void, iterable: string | Iterable): T[] { arr[arr.length] = item; } } - return (table.unpack || unpack)(arr); + return __TS__Unpack(arr); } diff --git a/src/lualib/Unpack.ts b/src/lualib/Unpack.ts new file mode 100644 index 000000000..99aedf253 --- /dev/null +++ b/src/lualib/Unpack.ts @@ -0,0 +1 @@ +const __TS__Unpack = table.unpack || unpack; diff --git a/src/lualib/declarations/math.d.ts b/src/lualib/declarations/math.d.ts index a0fb6c0e6..837f717f9 100644 --- a/src/lualib/declarations/math.d.ts +++ b/src/lualib/declarations/math.d.ts @@ -2,4 +2,10 @@ declare namespace math { /** @tupleReturn */ function modf(x: number): [number, number]; + + function atan(x: number): number; + // eslint-disable-next-line @typescript-eslint/unified-signatures + function atan(y: number, x?: number): number; + + function atan2(y: number, x: number): number; } diff --git a/src/lualib/tsconfig.json b/src/lualib/tsconfig.json index a9ca1cb4d..b9d6de8ce 100644 --- a/src/lualib/tsconfig.json +++ b/src/lualib/tsconfig.json @@ -11,7 +11,6 @@ }, "tstl": { "luaLibImport": "none", - "luaTarget": "5.1", "noHeader": true } } diff --git a/src/transformation/builtins/math.ts b/src/transformation/builtins/math.ts index 6ceae16bd..af742214f 100644 --- a/src/transformation/builtins/math.ts +++ b/src/transformation/builtins/math.ts @@ -1,8 +1,9 @@ import * as ts from "typescript"; -import * as lua from "../../LuaAST"; import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; import { TransformationContext } from "../context"; import { unsupportedProperty } from "../utils/diagnostics"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { PropertyCallExpression, transformArguments } from "../visitors/call"; export function transformMathProperty( @@ -37,22 +38,24 @@ export function transformMathCall( const expression = node.expression; const signature = context.checker.getResolvedSignature(node); const params = transformArguments(context, node.arguments, signature); + const math = lua.createIdentifier("math"); const expressionName = expression.name.text; switch (expressionName) { // Lua 5.3: math.atan(y, x) // Otherwise: math.atan2(y, x) case "atan2": { - const math = lua.createIdentifier("math"); - const methodName = context.options.luaTarget === LuaTarget.Lua53 ? "atan" : expressionName; - const method = lua.createStringLiteral(methodName); + if (context.luaTarget === LuaTarget.Universal) { + return transformLuaLibFunction(context, LuaLibFeature.MathAtan2, node, ...params); + } + + const method = lua.createStringLiteral(context.luaTarget === LuaTarget.Lua53 ? "atan" : "atan2"); return lua.createCallExpression(lua.createTableIndexExpression(math, method), params, node); } // (math.log(x) / Math.LNe) case "log10": case "log2": { - const math = lua.createIdentifier("math"); const log1 = lua.createTableIndexExpression(math, lua.createStringLiteral("log")); const logCall1 = lua.createCallExpression(log1, params); const e = lua.createNumericLiteral(expressionName === "log10" ? Math.LN10 : Math.LN2); @@ -61,7 +64,6 @@ export function transformMathCall( // math.log(1 + x) case "log1p": { - const math = lua.createIdentifier("math"); const log = lua.createStringLiteral("log"); const one = lua.createNumericLiteral(1); const add = lua.createBinaryExpression(one, params[0], lua.SyntaxKind.AdditionOperator); @@ -70,7 +72,6 @@ export function transformMathCall( // math.floor(x + 0.5) case "round": { - const math = lua.createIdentifier("math"); const floor = lua.createStringLiteral("floor"); const half = lua.createNumericLiteral(0.5); const add = lua.createBinaryExpression(params[0], half, lua.SyntaxKind.AdditionOperator); @@ -93,7 +94,6 @@ export function transformMathCall( case "sin": case "sqrt": case "tan": { - const math = lua.createIdentifier("math"); const method = lua.createStringLiteral(expressionName); return lua.createCallExpression(lua.createTableIndexExpression(math, method), params, node); } diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index e310a9e19..4ee4515c4 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -32,7 +32,7 @@ export class TransformationContext { public readonly resolver: EmitResolver; public readonly options: CompilerOptions = this.program.getCompilerOptions(); - public readonly luaTarget = this.options.luaTarget ?? LuaTarget.LuaJIT; + public readonly luaTarget = this.options.luaTarget ?? LuaTarget.Universal; public readonly isModule = ts.isExternalModule(this.sourceFile); public readonly isStrict = (this.options.alwaysStrict ?? this.options.strict) || diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts index a87de4c74..a5bdb35db 100644 --- a/src/transformation/utils/diagnostics.ts +++ b/src/transformation/utils/diagnostics.ts @@ -107,7 +107,7 @@ export const unsupportedRightShiftOperator = createDiagnosticFactory( const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ? "LuaJIT" : `Lua ${version}`); export const unsupportedForTarget = createDiagnosticFactory( - (functionality: string, version: LuaTarget) => + (functionality: string, version: Exclude) => `${functionality} is/are not supported for target ${getLuaTargetName(version)}.` ); diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index dfce4b2fc..e0c7682d5 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -6,6 +6,8 @@ import { TransformationContext } from "../context"; import { createExportedIdentifier, getIdentifierExportScope } from "./export"; import { peekScope, ScopeType } from "./scope"; import { isFunctionType } from "./typescript"; +import { transformLuaLibFunction } from "./lualib"; +import { LuaLibFeature } from "../../LuaLib"; export type OneToManyVisitorResult = T | T[] | undefined; export function unwrapVisitorResult(result: OneToManyVisitorResult): T[] { @@ -56,6 +58,10 @@ export function createUnpackCall( expression: lua.Expression, tsOriginal?: ts.Node ): lua.Expression { + if (context.luaTarget === LuaTarget.Universal) { + return transformLuaLibFunction(context, LuaLibFeature.Unpack, tsOriginal, expression); + } + const unpack = context.luaTarget === LuaTarget.Lua51 || context.luaTarget === LuaTarget.LuaJIT ? lua.createIdentifier("unpack") diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts index ffdf410ec..e284b2ea0 100644 --- a/src/transformation/visitors/binary-expression/bit.ts +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -62,6 +62,7 @@ export function transformBinaryBitOperation( operator: BitOperator ): lua.Expression { switch (context.luaTarget) { + case LuaTarget.Universal: case LuaTarget.Lua51: context.diagnostics.push(unsupportedForTarget(node, "Bitwise operations", LuaTarget.Lua51)); @@ -105,6 +106,7 @@ export function transformUnaryBitOperation( operator: lua.UnaryBitwiseOperator ): lua.Expression { switch (context.luaTarget) { + case LuaTarget.Universal: case LuaTarget.Lua51: context.diagnostics.push(unsupportedForTarget(node, "Bitwise operations", LuaTarget.Lua51)); diff --git a/src/transformation/visitors/break-continue.ts b/src/transformation/visitors/break-continue.ts index bde4ddd52..9211d4165 100644 --- a/src/transformation/visitors/break-continue.ts +++ b/src/transformation/visitors/break-continue.ts @@ -15,7 +15,7 @@ export const transformBreakStatement: FunctionVisitor = (brea }; export const transformContinueStatement: FunctionVisitor = (statement, context) => { - if (context.luaTarget === LuaTarget.Lua51) { + if (context.luaTarget === LuaTarget.Universal || context.luaTarget === LuaTarget.Lua51) { context.diagnostics.push(unsupportedForTarget(statement, "Continue statement", LuaTarget.Lua51)); } diff --git a/src/transformation/visitors/switch.ts b/src/transformation/visitors/switch.ts index 880f449bd..99ff3df7d 100644 --- a/src/transformation/visitors/switch.ts +++ b/src/transformation/visitors/switch.ts @@ -6,7 +6,7 @@ import { unsupportedForTarget } from "../utils/diagnostics"; import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope"; export const transformSwitchStatement: FunctionVisitor = (statement, context) => { - if (context.luaTarget === LuaTarget.Lua51) { + if (context.luaTarget === LuaTarget.Universal || context.luaTarget === LuaTarget.Lua51) { context.diagnostics.push(unsupportedForTarget(statement, "Switch statements", LuaTarget.Lua51)); } diff --git a/test/cli/parse.spec.ts b/test/cli/parse.spec.ts index 1053aae67..6b6349f9b 100644 --- a/test/cli/parse.spec.ts +++ b/test/cli/parse.spec.ts @@ -110,6 +110,7 @@ describe("command line", () => { ["luaLibImport", "inline", { luaLibImport: tstl.LuaLibImportKind.Inline }], ["luaLibImport", "require", { luaLibImport: tstl.LuaLibImportKind.Require }], + ["luaTarget", "universal", { luaTarget: tstl.LuaTarget.Universal }], ["luaTarget", "5.1", { luaTarget: tstl.LuaTarget.Lua51 }], ["luaTarget", "5.2", { luaTarget: tstl.LuaTarget.Lua52 }], ["luaTarget", "5.3", { luaTarget: tstl.LuaTarget.Lua53 }], @@ -216,6 +217,7 @@ describe("tsconfig", () => { ["luaLibImport", "inline", { luaLibImport: tstl.LuaLibImportKind.Inline }], ["luaLibImport", "require", { luaLibImport: tstl.LuaLibImportKind.Require }], + ["luaTarget", "universal", { luaTarget: tstl.LuaTarget.Universal }], ["luaTarget", "5.1", { luaTarget: tstl.LuaTarget.Lua51 }], ["luaTarget", "5.2", { luaTarget: tstl.LuaTarget.Lua52 }], ["luaTarget", "5.3", { luaTarget: tstl.LuaTarget.Lua53 }], diff --git a/test/unit/__snapshots__/loops.spec.ts.snap b/test/unit/__snapshots__/loops.spec.ts.snap index f1daffe65..e9b6be991 100644 --- a/test/unit/__snapshots__/loops.spec.ts.snap +++ b/test/unit/__snapshots__/loops.spec.ts.snap @@ -25,6 +25,19 @@ until not false" exports[`loop continue (do { continue; } while (false)) [5.1]: diagnostics 1`] = `"main.ts(1,6): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; +exports[`loop continue (do { continue; } while (false)) [universal]: code 1`] = ` +"repeat + do + do + goto __continue2 + end + ::__continue2:: + end +until not false" +`; + +exports[`loop continue (do { continue; } while (false)) [universal]: diagnostics 1`] = `"main.ts(1,6): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; + exports[`loop continue (for (;;) { continue; }) [5.1]: code 1`] = ` "do while true do @@ -38,6 +51,19 @@ end" exports[`loop continue (for (;;) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,12): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; +exports[`loop continue (for (;;) { continue; }) [universal]: code 1`] = ` +"do + while true do + do + goto __continue2 + end + ::__continue2:: + end +end" +`; + +exports[`loop continue (for (;;) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,12): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; + exports[`loop continue (for (const a in {}) { continue; }) [5.1]: code 1`] = ` "for a in pairs({}) do do @@ -49,6 +75,17 @@ end" exports[`loop continue (for (const a in {}) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; +exports[`loop continue (for (const a in {}) { continue; }) [universal]: code 1`] = ` +"for a in pairs({}) do + do + goto __continue2 + end + ::__continue2:: +end" +`; + +exports[`loop continue (for (const a in {}) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; + exports[`loop continue (for (const a of []) { continue; }) [5.1]: code 1`] = ` "for ____, a in ipairs({}) do do @@ -60,6 +97,17 @@ end" exports[`loop continue (for (const a of []) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; +exports[`loop continue (for (const a of []) { continue; }) [universal]: code 1`] = ` +"for ____, a in ipairs({}) do + do + goto __continue2 + end + ::__continue2:: +end" +`; + +exports[`loop continue (for (const a of []) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; + exports[`loop continue (while (false) { continue; }) [5.1]: code 1`] = ` "while false do do @@ -70,3 +118,14 @@ end" `; exports[`loop continue (while (false) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,17): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; + +exports[`loop continue (while (false) { continue; }) [universal]: code 1`] = ` +"while false do + do + goto __continue2 + end + ::__continue2:: +end" +`; + +exports[`loop continue (while (false) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,17): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; diff --git a/test/unit/builtins/math.spec.ts b/test/unit/builtins/math.spec.ts index 163095d9d..4d95e4afb 100644 --- a/test/unit/builtins/math.spec.ts +++ b/test/unit/builtins/math.spec.ts @@ -27,8 +27,11 @@ test.each(["E", "LN10", "LN2", "LOG10E", "LOG2E", "SQRT1_2", "SQRT2"])("Math.%s" const expectMathAtan2: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("math.atan2("); const expectMathAtan: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("math.atan("); +const expectLualibMathAtan2: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).toContain("__TS__MathAtan2("); util.testEachVersion("Math.atan2", () => util.testExpression`Math.atan2(4, 5)`, { + [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibMathAtan2), [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectMathAtan2), [tstl.LuaTarget.Lua51]: builder => builder.tap(expectMathAtan2), [tstl.LuaTarget.Lua52]: builder => builder.tap(expectMathAtan2), diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index 172150c47..0959d31b8 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -530,6 +530,7 @@ for (const testCase of [ expect(builder.getMainLuaCodeChunk()).toMatch("::__continue2::"); util.testEachVersion(`loop continue (${testCase})`, () => util.testModule(testCase), { + [tstl.LuaTarget.Universal]: builder => builder.expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]), [tstl.LuaTarget.Lua51]: builder => builder.expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]), [tstl.LuaTarget.Lua52]: expectContinueGotoLabel, [tstl.LuaTarget.Lua53]: expectContinueGotoLabel, diff --git a/test/unit/spread.spec.ts b/test/unit/spread.spec.ts index dec923d83..b3b7c95a0 100644 --- a/test/unit/spread.spec.ts +++ b/test/unit/spread.spec.ts @@ -5,6 +5,7 @@ import { formatCode } from "../util"; // TODO: Make some utils for testing other targets const expectUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toMatch(/[^.]unpack\(/); const expectTableUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("table.unpack"); +const expectLualibUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("__TS__Unpack"); const arrayLiteralCases = [ "1, 2, ...[3, 4, 5]", @@ -72,6 +73,7 @@ describe("in function call", () => { return foo(...array); `, { + [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibUnpack), [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack), [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack), [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack), @@ -82,6 +84,7 @@ describe("in function call", () => { describe("in array literal", () => { util.testEachVersion(undefined, () => util.testExpression`[...[0, 1, 2]]`, { + [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibUnpack), [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack), [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack), [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack),