From f744a372795e87bbb8fdf24563d78c358b11a1f3 Mon Sep 17 00:00:00 2001 From: Tom <26638278+tomblind@users.noreply.github.com> Date: Thu, 6 Jun 2019 08:09:49 -0600 Subject: [PATCH 1/3] typeof now uses lib function, with optimized path for comparisons --- src/LuaLib.ts | 1 + src/LuaTransformer.ts | 43 ++++++++++++++++++---------- src/lualib/TypeOf.ts | 12 ++++++++ test/unit/typechecking.spec.ts | 51 +++++++++++++++++++++++++++++++++- 4 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 src/lualib/TypeOf.ts diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 658664488..23a9711ad 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -55,6 +55,7 @@ export enum LuaLibFeature { StringStartsWith = "StringStartsWith", Symbol = "Symbol", SymbolRegistry = "SymbolRegistry", + TypeOf = "TypeOf", } const luaLibDependencies: { [lib in LuaLibFeature]?: LuaLibFeature[] } = { diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 09317db82..6a6278f47 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -2655,6 +2655,22 @@ export class LuaTransformer { } } + protected transformTypeOfLiteralComparison( + typeOfExpression: ts.TypeOfExpression, + comparedExpression: tstl.StringLiteral, + operator: ts.BinaryOperator, + tsOriginal: ts.Node + ): ExpressionVisitResult { + if (comparedExpression.value === "object") { + comparedExpression.value = "table"; + } else if (comparedExpression.value === "undefined") { + comparedExpression.value = "nil"; + } + const innerExpression = this.transformExpression(typeOfExpression.expression); + const typeCall = tstl.createCallExpression(tstl.createIdentifier("type"), [innerExpression], typeOfExpression); + return this.transformBinaryOperation(typeCall, comparedExpression, operator, tsOriginal); + } + public transformBinaryExpression(expression: ts.BinaryExpression): ExpressionVisitResult { // Check if this is an assignment token, then handle accordingly @@ -2673,14 +2689,15 @@ export class LuaTransformer { const rhs = this.transformExpression(expression.right); // Transpile operators - switch (expression.operatorToken.kind) { + const operator = expression.operatorToken.kind; + switch (operator) { case ts.SyntaxKind.AmpersandToken: case ts.SyntaxKind.BarToken: case ts.SyntaxKind.CaretToken: case ts.SyntaxKind.LessThanLessThanToken: case ts.SyntaxKind.GreaterThanGreaterThanToken: case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - return this.transformBinaryBitOperation(expression, lhs, rhs, expression.operatorToken.kind); + return this.transformBinaryBitOperation(expression, lhs, rhs, operator); case ts.SyntaxKind.PlusToken: case ts.SyntaxKind.AmpersandAmpersandToken: case ts.SyntaxKind.BarBarToken: @@ -2689,6 +2706,7 @@ export class LuaTransformer { case ts.SyntaxKind.AsteriskAsteriskToken: case ts.SyntaxKind.SlashToken: case ts.SyntaxKind.PercentToken: + return this.transformBinaryOperation(lhs, rhs, operator, expression); case ts.SyntaxKind.GreaterThanToken: case ts.SyntaxKind.GreaterThanEqualsToken: case ts.SyntaxKind.LessThanToken: @@ -2697,7 +2715,13 @@ export class LuaTransformer { case ts.SyntaxKind.EqualsEqualsEqualsToken: case ts.SyntaxKind.ExclamationEqualsToken: case ts.SyntaxKind.ExclamationEqualsEqualsToken: - return this.transformBinaryOperation(lhs, rhs, expression.operatorToken.kind, expression); + // Custom handling for 'typeof(foo) === "type"' + if (ts.isTypeOfExpression(expression.left) && tstl.isStringLiteral(rhs)) { + return this.transformTypeOfLiteralComparison(expression.left, rhs, operator, expression); + } else if (ts.isTypeOfExpression(expression.right) && tstl.isStringLiteral(lhs)) { + return this.transformTypeOfLiteralComparison(expression.right, lhs, operator, expression); + } + return this.transformBinaryOperation(lhs, rhs, operator, expression); case ts.SyntaxKind.EqualsToken: return this.transformAssignmentExpression(expression); case ts.SyntaxKind.InKeyword: @@ -2736,7 +2760,7 @@ export class LuaTransformer { ); default: - throw TSTLErrors.UnsupportedKind("binary operator", expression.operatorToken.kind, expression); + throw TSTLErrors.UnsupportedKind("binary operator", operator, expression); } } @@ -4489,16 +4513,7 @@ export class LuaTransformer { public transformTypeOfExpression(expression: ts.TypeOfExpression): ExpressionVisitResult { const innerExpression = this.transformExpression(expression.expression); - const typeFunctionIdentifier = tstl.createIdentifier("type"); - const typeCall = tstl.createCallExpression(typeFunctionIdentifier, [innerExpression]); - const tableString = tstl.createStringLiteral("table"); - const objectString = tstl.createStringLiteral("object"); - const condition = tstl.createBinaryExpression(typeCall, tableString, tstl.SyntaxKind.EqualityOperator); - const andClause = tstl.createBinaryExpression(condition, objectString, tstl.SyntaxKind.AndOperator); - - return tstl.createParenthesizedExpression( - tstl.createBinaryExpression(andClause, tstl.cloneNode(typeCall), tstl.SyntaxKind.OrOperator, expression) - ); + return this.transformLuaLibFunction(LuaLibFeature.TypeOf, expression, innerExpression); } public transformSpreadElement(expression: ts.SpreadElement): ExpressionVisitResult { diff --git a/src/lualib/TypeOf.ts b/src/lualib/TypeOf.ts new file mode 100644 index 000000000..af531d732 --- /dev/null +++ b/src/lualib/TypeOf.ts @@ -0,0 +1,12 @@ +declare function type(this: void, value: unknown): string; + +function __TS__TypeOf(this: void, value: unknown): string { + const luaType = type(value); + if (luaType === "table") { + return "object"; + } else if (luaType === "nil") { + return "undefined"; + } else { + return luaType; + } +} diff --git a/test/unit/typechecking.spec.ts b/test/unit/typechecking.spec.ts index 3d5014336..51257d84c 100644 --- a/test/unit/typechecking.spec.ts +++ b/test/unit/typechecking.spec.ts @@ -40,7 +40,7 @@ test("typeof function", () => { test.each(["null", "undefined"])("typeof undefined (%p)", inp => { const result = util.transpileAndExecute(`return typeof ${inp};`); - expect(result).toBe("nil"); + expect(result).toBe("undefined"); }); test("instanceof", () => { @@ -140,3 +140,52 @@ test("instanceof Symbol.hasInstance", () => { expect(result).toBe(true); }); + +test.each([ + { expression: "{}", operator: "===", compareTo: "object", expectResult: "TRUE" }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: "FALSE" }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: "TRUE" }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: "FALSE" }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: "TRUE" }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: "FALSE" }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: "TRUE" }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: "TRUE" }, + { expression: "1", operator: "===", compareTo: "number", expectResult: "TRUE" }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: "TRUE" }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: "TRUE" }, +])("typeof literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { + const code = ` + let val = ${expression}; + if (typeof val ${operator} "${compareTo}") { + return "TRUE"; + } else { + return "FALSE"; + }`; + + expect(util.transpileAndExecute(code)).toBe(expectResult); +}); + +test.each([ + { expression: "{}", operator: "===", compareTo: "object", expectResult: "TRUE" }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: "FALSE" }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: "TRUE" }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: "FALSE" }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: "TRUE" }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: "FALSE" }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: "TRUE" }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: "TRUE" }, + { expression: "1", operator: "===", compareTo: "number", expectResult: "TRUE" }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: "TRUE" }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: "TRUE" }, +])("typeof non-literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { + const code = ` + let val = ${expression}; + let compareTo = "${compareTo}"; + if (typeof val ${operator} compareTo) { + return "TRUE"; + } else { + return "FALSE"; + }`; + + expect(util.transpileAndExecute(code)).toBe(expectResult); +}); From 89bc7bb3f24b38c40c68b86df1cbc31c637d44ce Mon Sep 17 00:00:00 2001 From: Tom <26638278+tomblind@users.noreply.github.com> Date: Thu, 6 Jun 2019 13:33:19 -0600 Subject: [PATCH 2/3] simplified tests --- test/unit/typechecking.spec.ts | 56 +++++++++++++++------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/test/unit/typechecking.spec.ts b/test/unit/typechecking.spec.ts index 51257d84c..166921e92 100644 --- a/test/unit/typechecking.spec.ts +++ b/test/unit/typechecking.spec.ts @@ -142,50 +142,42 @@ test("instanceof Symbol.hasInstance", () => { }); test.each([ - { expression: "{}", operator: "===", compareTo: "object", expectResult: "TRUE" }, - { expression: "{}", operator: "!==", compareTo: "object", expectResult: "FALSE" }, - { expression: "{}", operator: "==", compareTo: "object", expectResult: "TRUE" }, - { expression: "{}", operator: "!=", compareTo: "object", expectResult: "FALSE" }, - { expression: "{}", operator: "<=", compareTo: "object", expectResult: "TRUE" }, - { expression: "{}", operator: "<", compareTo: "object", expectResult: "FALSE" }, - { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: "TRUE" }, - { expression: "() => {}", operator: "===", compareTo: "function", expectResult: "TRUE" }, - { expression: "1", operator: "===", compareTo: "number", expectResult: "TRUE" }, - { expression: "true", operator: "===", compareTo: "boolean", expectResult: "TRUE" }, - { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: "TRUE" }, + { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, + { expression: "1", operator: "===", compareTo: "number", expectResult: true }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, ])("typeof literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { const code = ` let val = ${expression}; - if (typeof val ${operator} "${compareTo}") { - return "TRUE"; - } else { - return "FALSE"; - }`; + return typeof val ${operator} "${compareTo}";`; expect(util.transpileAndExecute(code)).toBe(expectResult); }); test.each([ - { expression: "{}", operator: "===", compareTo: "object", expectResult: "TRUE" }, - { expression: "{}", operator: "!==", compareTo: "object", expectResult: "FALSE" }, - { expression: "{}", operator: "==", compareTo: "object", expectResult: "TRUE" }, - { expression: "{}", operator: "!=", compareTo: "object", expectResult: "FALSE" }, - { expression: "{}", operator: "<=", compareTo: "object", expectResult: "TRUE" }, - { expression: "{}", operator: "<", compareTo: "object", expectResult: "FALSE" }, - { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: "TRUE" }, - { expression: "() => {}", operator: "===", compareTo: "function", expectResult: "TRUE" }, - { expression: "1", operator: "===", compareTo: "number", expectResult: "TRUE" }, - { expression: "true", operator: "===", compareTo: "boolean", expectResult: "TRUE" }, - { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: "TRUE" }, + { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, + { expression: "1", operator: "===", compareTo: "number", expectResult: true }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, ])("typeof non-literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { const code = ` let val = ${expression}; let compareTo = "${compareTo}"; - if (typeof val ${operator} compareTo) { - return "TRUE"; - } else { - return "FALSE"; - }`; + return typeof val ${operator} compareTo;`; expect(util.transpileAndExecute(code)).toBe(expectResult); }); From 613fb0542259dad33c33e9f00a9e596f594ea3e7 Mon Sep 17 00:00:00 2001 From: Tom <26638278+tomblind@users.noreply.github.com> Date: Thu, 6 Jun 2019 13:33:36 -0600 Subject: [PATCH 3/3] refactored transformBinaryExpression a bit --- src/LuaTransformer.ts | 55 +++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 6a6278f47..dc3b7f1ac 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -2671,10 +2671,26 @@ export class LuaTransformer { return this.transformBinaryOperation(typeCall, comparedExpression, operator, tsOriginal); } + protected transformComparisonExpression(expression: ts.BinaryExpression): ExpressionVisitResult { + const left = this.transformExpression(expression.left); + const right = this.transformExpression(expression.right); + const operator = expression.operatorToken.kind; + + // Custom handling for 'typeof(foo) === "type"' + if (ts.isTypeOfExpression(expression.left) && tstl.isStringLiteral(right)) { + return this.transformTypeOfLiteralComparison(expression.left, right, operator, expression); + } else if (ts.isTypeOfExpression(expression.right) && tstl.isStringLiteral(left)) { + return this.transformTypeOfLiteralComparison(expression.right, left, operator, expression); + } + + return this.transformBinaryOperation(left, right, operator, expression); + } + public transformBinaryExpression(expression: ts.BinaryExpression): ExpressionVisitResult { - // Check if this is an assignment token, then handle accordingly + const operator = expression.operatorToken.kind; - const [isCompound, replacementOperator] = tsHelper.isBinaryAssignmentToken(expression.operatorToken.kind); + // Check if this is an assignment token, then handle accordingly + const [isCompound, replacementOperator] = tsHelper.isBinaryAssignmentToken(operator); if (isCompound && replacementOperator) { return this.transformCompoundAssignmentExpression( expression, @@ -2685,11 +2701,7 @@ export class LuaTransformer { ); } - const lhs = this.transformExpression(expression.left); - const rhs = this.transformExpression(expression.right); - // Transpile operators - const operator = expression.operatorToken.kind; switch (operator) { case ts.SyntaxKind.AmpersandToken: case ts.SyntaxKind.BarToken: @@ -2697,7 +2709,6 @@ export class LuaTransformer { case ts.SyntaxKind.LessThanLessThanToken: case ts.SyntaxKind.GreaterThanGreaterThanToken: case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - return this.transformBinaryBitOperation(expression, lhs, rhs, operator); case ts.SyntaxKind.PlusToken: case ts.SyntaxKind.AmpersandAmpersandToken: case ts.SyntaxKind.BarBarToken: @@ -2705,8 +2716,12 @@ export class LuaTransformer { case ts.SyntaxKind.AsteriskToken: case ts.SyntaxKind.AsteriskAsteriskToken: case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.PercentToken: + case ts.SyntaxKind.PercentToken: { + const lhs = this.transformExpression(expression.left); + const rhs = this.transformExpression(expression.right); return this.transformBinaryOperation(lhs, rhs, operator, expression); + } + case ts.SyntaxKind.GreaterThanToken: case ts.SyntaxKind.GreaterThanEqualsToken: case ts.SyntaxKind.LessThanToken: @@ -2715,16 +2730,14 @@ export class LuaTransformer { case ts.SyntaxKind.EqualsEqualsEqualsToken: case ts.SyntaxKind.ExclamationEqualsToken: case ts.SyntaxKind.ExclamationEqualsEqualsToken: - // Custom handling for 'typeof(foo) === "type"' - if (ts.isTypeOfExpression(expression.left) && tstl.isStringLiteral(rhs)) { - return this.transformTypeOfLiteralComparison(expression.left, rhs, operator, expression); - } else if (ts.isTypeOfExpression(expression.right) && tstl.isStringLiteral(lhs)) { - return this.transformTypeOfLiteralComparison(expression.right, lhs, operator, expression); - } - return this.transformBinaryOperation(lhs, rhs, operator, expression); + return this.transformComparisonExpression(expression); + case ts.SyntaxKind.EqualsToken: return this.transformAssignmentExpression(expression); - case ts.SyntaxKind.InKeyword: + + case ts.SyntaxKind.InKeyword: { + const lhs = this.transformExpression(expression.left); + const rhs = this.transformExpression(expression.right); const indexExpression = tstl.createTableIndexExpression(rhs, lhs); return tstl.createBinaryExpression( indexExpression, @@ -2732,8 +2745,11 @@ export class LuaTransformer { tstl.SyntaxKind.InequalityOperator, expression ); + } - case ts.SyntaxKind.InstanceOfKeyword: + case ts.SyntaxKind.InstanceOfKeyword: { + const lhs = this.transformExpression(expression.left); + const rhs = this.transformExpression(expression.right); const rhsType = this.checker.getTypeAtLocation(expression.right); const decorators = tsHelper.getCustomDecorators(rhsType, this.checker); @@ -2751,13 +2767,16 @@ export class LuaTransformer { } return this.transformLuaLibFunction(LuaLibFeature.InstanceOf, expression, lhs, rhs); + } - case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.CommaToken: { + const rhs = this.transformExpression(expression.right); return this.createImmediatelyInvokedFunctionExpression( this.statementVisitResultToArray(this.transformExpressionStatement(expression.left)), rhs, expression ); + } default: throw TSTLErrors.UnsupportedKind("binary operator", operator, expression);