diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 467131acb..1133af8bb 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -26,7 +26,6 @@ export enum LuaLibFeature { StringReplace = "StringReplace", StringSplit = "StringSplit", Symbol = "Symbol", - Ternary = "Ternary", } const luaLibDependencies: { [lib in LuaLibFeature]?: LuaLibFeature[] } = { diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 44b49a42e..8399bf9a0 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -453,4 +453,28 @@ export class TSHelper { public static isDefaultArrayPropertyName(methodName: string): boolean { return defaultArrayPropertyNames.has(methodName); } + + public static isFalsible(type: ts.Type, strictNullChecks: boolean): boolean { + const falsibleFlags = ts.TypeFlags.Boolean + | ts.TypeFlags.BooleanLiteral + | ts.TypeFlags.Undefined + | ts.TypeFlags.Null + | ts.TypeFlags.Never + | ts.TypeFlags.Void + | ts.TypeFlags.Any; + + if (type.flags & falsibleFlags) { + return true; + } else if (!strictNullChecks && !type.isLiteral()) { + return true; + } else if (type.isUnion()) { + for (const subType of type.types) { + if (this.isFalsible(subType, strictNullChecks)) { + return true; + } + } + } + + return false; + } } diff --git a/src/Transpiler.ts b/src/Transpiler.ts index 561733492..ed817816e 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -780,7 +780,7 @@ export abstract class LuaTranspiler { return this.transpileBinaryExpression(node as ts.BinaryExpression, brackets); case ts.SyntaxKind.ConditionalExpression: // Add brackets to preserve ordering - return this.transpileConditionalExpression(node as ts.ConditionalExpression, brackets); + return this.transpileConditionalExpression(node as ts.ConditionalExpression); case ts.SyntaxKind.CallExpression: return this.transpileCallExpression(node as ts.CallExpression); case ts.SyntaxKind.PropertyAccessExpression: @@ -1030,13 +1030,22 @@ export abstract class LuaTranspiler { return parts.join(".."); } - public transpileConditionalExpression(node: ts.ConditionalExpression, brackets?: boolean): string { + public transpileProtectedConditionalExpression(node: ts.ConditionalExpression): string { const condition = this.transpileExpression(node.condition); const val1 = this.transpileExpression(node.whenTrue); const val2 = this.transpileExpression(node.whenFalse); + return `((${condition}) and function() return ${val1}; end or function() return ${val2}; end)()`; + } - return this.transpileLuaLibFunction(LuaLibFeature.Ternary, condition, - `function() return ${val1} end`, `function() return ${val2} end`); + public transpileConditionalExpression(node: ts.ConditionalExpression): string { + const isStrict = this.options.strict || this.options.strictNullChecks; + if (tsHelper.isFalsible(this.checker.getTypeAtLocation(node.whenTrue), isStrict)) { + return this.transpileProtectedConditionalExpression(node); + } + const condition = this.transpileExpression(node.condition); + const val1 = this.transpileExpression(node.whenTrue); + const val2 = this.transpileExpression(node.whenFalse); + return `((${condition}) and (${val1}) or (${val2}))`; } public transpileBinaryAssignmentExpression( diff --git a/src/lualib/Ternary.ts b/src/lualib/Ternary.ts deleted file mode 100644 index f1331f4f4..000000000 --- a/src/lualib/Ternary.ts +++ /dev/null @@ -1,7 +0,0 @@ -function __TS__Ternary(condition: boolean, cb1: () => T, cb2: () => T): T { - if (condition) { - return cb1(); - } else { - return cb2(); - } -} diff --git a/src/targets/Transpiler.JIT.ts b/src/targets/Transpiler.JIT.ts index a23e3a8e2..466be79a5 100644 --- a/src/targets/Transpiler.JIT.ts +++ b/src/targets/Transpiler.JIT.ts @@ -44,4 +44,12 @@ export class LuaTranspilerJIT extends LuaTranspiler52 { public transpileSpreadElement(node: ts.SpreadElement): string { return "unpack(" + this.transpileExpression(node.expression) + ")"; } + + /** @override */ + public transpileProtectedConditionalExpression(node: ts.ConditionalExpression): string { + const condition = this.transpileExpression(node.condition); + const val1 = this.transpileExpression(node.whenTrue); + const val2 = this.transpileExpression(node.whenFalse); + return `((${condition}) and {${val1}} or {${val2}})[1]`; + } } diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 6f9c2ba30..2e86beac7 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -192,6 +192,36 @@ export class ExpressionTests { Expect(util.transpileString("undefined")).toBe("nil;"); } + @TestCase("true ? 'a' : 'b'", "a") + @TestCase("false ? 'a' : 'b'", "b") + @TestCase("true ? false : true", false) + @TestCase("false ? false : true", true) + @TestCase("true ? literalValue : true", "literal") + @TestCase("true ? variableValue : true", undefined) + @TestCase("true ? maybeUndefinedValue : true", undefined) + @TestCase("true ? maybeBooleanValue : true", false) + @TestCase("true ? maybeUndefinedValue : true", undefined, { strictNullChecks: true }) + @TestCase("true ? maybeBooleanValue : true", false, { strictNullChecks: true }) + @TestCase("true ? undefined : true", undefined, { strictNullChecks: true }) + @TestCase("true ? null : true", undefined, { strictNullChecks: true }) + @TestCase("true ? false : true", false, { luaTarget: LuaTarget.Lua51 }) + @TestCase("false ? false : true", true, { luaTarget: LuaTarget.Lua51 }) + @TestCase("true ? undefined : true", undefined, { luaTarget: LuaTarget.Lua51 }) + @TestCase("true ? false : true", false, { luaTarget: LuaTarget.LuaJIT }) + @TestCase("false ? false : true", true, { luaTarget: LuaTarget.LuaJIT }) + @TestCase("true ? undefined : true", undefined, { luaTarget: LuaTarget.LuaJIT }) + @Test("Ternary operator") + public ternaryOperator(input: string, expected: any, options?: ts.CompilerOptions): void { + const source = `const literalValue = 'literal';` + + `let variableValue:string;` + + `let maybeBooleanValue:string|boolean = false;` + + `let maybeUndefinedValue:string|undefined;` + + `return ${input};`; + const lua = util.transpileString(source, options); + const result = util.executeLua(lua); + Expect(result).toBe(expected); + } + @TestCase("inst.field", 8) @TestCase("inst.field + 3", 8 + 3) @TestCase("inst.field * 3", 8 * 3) @@ -260,7 +290,7 @@ export class ExpressionTests { @TestCase("inst.superBaseField", 4) @Test("Inherited accessors") public inheritedAccessors(expression: string, expected: any): void { - const source = `class MyBaseClass {` + const source = `class MyBaseClass {` + ` public _baseField: number;` + ` public get baseField(): number { return this._baseField + 6; }` + ` public set baseField(v: number) { this._baseField = v; }` @@ -269,13 +299,13 @@ export class ExpressionTests { + ` public _field: number;` + ` public get field(): number { return this._field + 4; }` + ` public set field(v: number) { this._field = v; }` - + `}` + + `}` + `class MySuperClass extends MyClass {` + ` public _superField: number;` + ` public get superField(): number { return this._superField + 2; }` + ` public set superField(v: number) { this._superField = v; }` + ` public get superBaseField() { return this.baseField - 3; }` - + `}` + + `}` + `var inst = new MySuperClass();` + `inst.baseField = 1;` + `inst.field = 2;`