From 32192c45bfeb9a20ae9b449bb7eeeff52aa7a07d Mon Sep 17 00:00:00 2001 From: ark120202 Date: Mon, 25 Nov 2019 21:01:55 +0000 Subject: [PATCH 1/4] Implement Number.prototype.toString(radix) --- src/LuaLib.ts | 1 + src/LuaTransformer.ts | 25 +++++++++++++-- src/lualib/NumberToString.ts | 48 +++++++++++++++++++++++++++++ src/lualib/declarations/math.d.ts | 5 +++ src/lualib/declarations/string.d.ts | 1 + test/unit/builtins/numbers.spec.ts | 12 ++++++++ 6 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/lualib/NumberToString.ts create mode 100644 src/lualib/declarations/math.d.ts diff --git a/src/LuaLib.ts b/src/LuaLib.ts index c877dd9cc..b50ab1e75 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -40,6 +40,7 @@ export enum LuaLibFeature { Number = "Number", NumberIsFinite = "NumberIsFinite", NumberIsNaN = "NumberIsNaN", + NumberToString = "NumberToString", ObjectAssign = "ObjectAssign", ObjectEntries = "ObjectEntries", ObjectFromEntries = "ObjectFromEntries", diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index e460993b7..5479fc889 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -4295,7 +4295,7 @@ export class LuaTransformer { } if (tsHelper.isStandardLibraryType(ownerType, "NumberConstructor", this.program)) { - return this.transformNumberCallExpression(node); + return this.transformNumberExpression(node); } const classDecorators = tsHelper.getCustomDecorators(ownerType, this.checker); @@ -4308,6 +4308,10 @@ export class LuaTransformer { return this.transformStringCallExpression(node); } + if (tsHelper.isNumberType(ownerType, this.checker, this.program)) { + return this.transformNumberCallExpression(node); + } + // if ownerType is a array, use only supported functions if (tsHelper.isExplicitArrayType(ownerType, this.checker, this.program)) { return this.transformArrayCallExpression(node); @@ -4883,6 +4887,23 @@ export class LuaTransformer { ); } + protected transformNumberCallExpression(node: ts.CallExpression): tstl.Expression { + const expression = node.expression as ts.PropertyAccessExpression; + const signature = this.checker.getResolvedSignature(node); + const params = this.transformArguments(node.arguments, signature); + const caller = this.transformExpression(expression.expression); + + const expressionName = expression.name.text; + switch (expressionName) { + case "toString": + return params.length === 0 + ? tstl.createCallExpression(tstl.createIdentifier("tostring"), [caller], node) + : this.transformLuaLibFunction(LuaLibFeature.NumberToString, node, caller, ...params); + default: + throw TSTLErrors.UnsupportedProperty("number", expressionName, node); + } + } + // Transpile a String._ property protected transformStringExpression(identifier: ts.Identifier): ExpressionVisitResult { const identifierString = identifier.text; @@ -5015,7 +5036,7 @@ export class LuaTransformer { } // Transpile a Number._ property - protected transformNumberCallExpression(expression: ts.CallExpression): tstl.CallExpression { + protected transformNumberExpression(expression: ts.CallExpression): tstl.CallExpression { const method = expression.expression as ts.PropertyAccessExpression; const parameters = this.transformArguments(expression.arguments); const methodName = method.name.text; diff --git a/src/lualib/NumberToString.ts b/src/lualib/NumberToString.ts new file mode 100644 index 000000000..04f67d4c4 --- /dev/null +++ b/src/lualib/NumberToString.ts @@ -0,0 +1,48 @@ +// tslint:disable-next-line: variable-name +const ____radixChars = "0123456789abcdefghijklmnopqrstuvwxyz"; + +function __TS__NumberToString(this: number, radix?: number): string { + if (radix === undefined || radix === 10 || this === Infinity || this === -Infinity || this !== this) { + return this.toString(); + } + + radix = Math.floor(radix); + if (radix < 2 || radix > 36) { + // tslint:disable-next-line: no-string-throw + throw "toString() radix argument must be between 2 and 36"; + } + + let [integer, fraction] = math.modf(Math.abs(this)); + + let result = ""; + if (radix === 8) { + result = string.format("%o", integer); + } else if (radix === 16) { + result = string.format("%x", integer); + } else { + do { + result = ____radixChars[integer % radix] + result; + integer = Math.floor(integer / radix); + } while (integer !== 0); + } + + // https://github.com/v8/v8/blob/f78e8d43c224847fa56b3220a90be250fc0f0d6e/src/numbers/conversions.cc#L1221 + if (fraction !== 0) { + result += "."; + let delta = 1e-16; + do { + fraction *= radix; + delta *= radix; + const digit = Math.floor(fraction); + result += ____radixChars[digit]; + fraction -= digit; + // TODO: Round to even + } while (fraction >= delta); + } + + if (this < 0) { + result = "-" + result; + } + + return result; +} diff --git a/src/lualib/declarations/math.d.ts b/src/lualib/declarations/math.d.ts new file mode 100644 index 000000000..a0fb6c0e6 --- /dev/null +++ b/src/lualib/declarations/math.d.ts @@ -0,0 +1,5 @@ +/** @noSelf */ +declare namespace math { + /** @tupleReturn */ + function modf(x: number): [number, number]; +} diff --git a/src/lualib/declarations/string.d.ts b/src/lualib/declarations/string.d.ts index 1003f5450..a21500d28 100644 --- a/src/lualib/declarations/string.d.ts +++ b/src/lualib/declarations/string.d.ts @@ -8,4 +8,5 @@ declare namespace string { n?: number ): [string, number]; function sub(s: string, i: number, j?: number): string; + function format(formatstring: string, ...args: any[]): string; } diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index 4369f22c6..d7f26d6b1 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -48,6 +48,18 @@ describe("Number", () => { }); }); +const toStringRadixes = [undefined, 10, 2, 8, 9, 16, 17, 36, 36.9]; +const toStringValues = [...numberCases, 1024, 1.2, NaN]; +// TODO: flatMap +const toStringPairs = toStringValues.reduce>( + (results, value) => [...results, ...toStringRadixes.map(radix => [value, radix] as const)], + [] +); + +test.each(toStringPairs)("(%p).toString(%p)", (value, radix) => { + util.testExpressionTemplate`(${value}).toString(${radix})`.expectToMatchJsResult(); +}); + test.each(cases)("isNaN(%p)", value => { util.testExpressionTemplate`isNaN(${value} as any)`.expectToMatchJsResult(); }); From fa3021ad41a78c59e32a460cdb7cd5c6c03143f2 Mon Sep 17 00:00:00 2001 From: ark120202 Date: Mon, 25 Nov 2019 21:56:57 +0000 Subject: [PATCH 2/4] Use `flatMap` to make combinations --- src/utils.ts | 15 +++++++++++++++ test/unit/builtins/numbers.spec.ts | 7 ++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 0fe3f68b6..204a1d16c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1 +1,16 @@ export const normalizeSlashes = (filePath: string) => filePath.replace(/\\/g, "/"); + +export function flatMap(array: readonly T[], callback: (value: T, index: number) => U | readonly U[]): U[] { + const result: U[] = []; + + for (const [index, value] of array.entries()) { + const mappedValue = callback(value, index); + if (Array.isArray(mappedValue)) { + result.push(...mappedValue); + } else { + result[result.length] = mappedValue as U; + } + } + + return result; +} diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index d7f26d6b1..a7344c540 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -1,3 +1,4 @@ +import { flatMap } from "../../../src/utils"; import * as util from "../../util"; test.each([ @@ -50,11 +51,7 @@ describe("Number", () => { const toStringRadixes = [undefined, 10, 2, 8, 9, 16, 17, 36, 36.9]; const toStringValues = [...numberCases, 1024, 1.2, NaN]; -// TODO: flatMap -const toStringPairs = toStringValues.reduce>( - (results, value) => [...results, ...toStringRadixes.map(radix => [value, radix] as const)], - [] -); +const toStringPairs = flatMap(toStringValues, value => toStringRadixes.map(radix => [value, radix] as const)); test.each(toStringPairs)("(%p).toString(%p)", (value, radix) => { util.testExpressionTemplate`(${value}).toString(${radix})`.expectToMatchJsResult(); From c1e034c71455db53ea198421b385b094336db5dd Mon Sep 17 00:00:00 2001 From: ark120202 Date: Mon, 25 Nov 2019 22:01:21 +0000 Subject: [PATCH 3/4] Test special values separately --- test/unit/builtins/numbers.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index a7344c540..7a5d5c731 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -50,13 +50,17 @@ describe("Number", () => { }); const toStringRadixes = [undefined, 10, 2, 8, 9, 16, 17, 36, 36.9]; -const toStringValues = [...numberCases, 1024, 1.2, NaN]; +const toStringValues = [-1, 0, 1, 1.5, 1024, 1.2]; const toStringPairs = flatMap(toStringValues, value => toStringRadixes.map(radix => [value, radix] as const)); test.each(toStringPairs)("(%p).toString(%p)", (value, radix) => { util.testExpressionTemplate`(${value}).toString(${radix})`.expectToMatchJsResult(); }); +test.each([NaN, Infinity, -Infinity])("%p.toString(2)", value => { + util.testExpressionTemplate`(${value}).toString(2)`.expectToMatchJsResult(); +}); + test.each(cases)("isNaN(%p)", value => { util.testExpressionTemplate`isNaN(${value} as any)`.expectToMatchJsResult(); }); From 936945c4b30f715cacd25f78f7cc4643dc0d0d37 Mon Sep 17 00:00:00 2001 From: ark120202 Date: Mon, 25 Nov 2019 22:02:00 +0000 Subject: [PATCH 4/4] Add link to spec --- src/lualib/NumberToString.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lualib/NumberToString.ts b/src/lualib/NumberToString.ts index 04f67d4c4..3ad9a3d5e 100644 --- a/src/lualib/NumberToString.ts +++ b/src/lualib/NumberToString.ts @@ -1,6 +1,7 @@ // tslint:disable-next-line: variable-name const ____radixChars = "0123456789abcdefghijklmnopqrstuvwxyz"; +// https://www.ecma-international.org/ecma-262/10.0/index.html#sec-number.prototype.tostring function __TS__NumberToString(this: number, radix?: number): string { if (radix === undefined || radix === 10 || this === Infinity || this === -Infinity || this !== this) { return this.toString();