diff --git a/compiler/cpp/src/thrift/generate/t_js_generator.cc b/compiler/cpp/src/thrift/generate/t_js_generator.cc index a7ee220282f..c901a3ffe4f 100644 --- a/compiler/cpp/src/thrift/generate/t_js_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_js_generator.cc @@ -70,6 +70,10 @@ class t_js_generator : public t_oop_generator { gen_es6_ = false; gen_esm_ = false; gen_episode_file_ = false; + // Default true: when generating for js:node, int64 values are emitted as + // native BigInt literals and the node-int64 import is omitted. Callers + // opt out with bigint=false to retain the legacy `new Int64(...)` output. + gen_bigint_ = true; bool with_ns_ = false; @@ -90,11 +94,27 @@ class t_js_generator : public t_oop_generator { parse_imports(program, iter->second); } else if (iter->first.compare("thrift_package_output_directory") == 0) { parse_thrift_package_output_directory(iter->second); + } else if (iter->first.compare("bigint") == 0) { + if (iter->second.empty() || iter->second.compare("true") == 0) { + gen_bigint_ = true; + } else if (iter->second.compare("false") == 0) { + gen_bigint_ = false; + } else { + throw std::invalid_argument( + "invalid value for js:bigint, must be true or false"); + } } else { throw std::invalid_argument("unknown option js:" + iter->first); } } + // BigInt-mode code generation is only meaningful for the node generator. + // Force it off for plain browser-JS so the default-true setting does not + // affect callers that pass plain `--gen js`. + if (!gen_node_) { + gen_bigint_ = false; + } + if (gen_es6_ && gen_jquery_) { throw std::invalid_argument("invalid switch: [-gen js:es6,jquery] options not compatible"); } @@ -383,6 +403,13 @@ class t_js_generator : public t_oop_generator { */ bool gen_esm_; + /** + * True (default) if int64 values should be generated as native BigInt + * literals. False emits the legacy `new Int64(...)` from node-int64. + * Only meaningful when gen_node_ is true. + */ + bool gen_bigint_; + /** * True if we will generate an episode file. */ @@ -538,10 +565,14 @@ string t_js_generator::js_includes() { } } if (gen_esm_) { - result += "import Int64 from 'node-int64';\n"; + if (!gen_bigint_) { + result += "import Int64 from 'node-int64';\n"; + } result += "import { v4 as uuid } from 'uuid';"; } else { - result += js_const_type_ + "Int64 = require('node-int64');\n"; + if (!gen_bigint_) { + result += js_const_type_ + "Int64 = require('node-int64');\n"; + } result += js_const_type_ + "uuid = require('uuid').v4;\n"; } return result; @@ -556,13 +587,17 @@ string t_js_generator::js_includes() { */ string t_js_generator::ts_includes() { if (gen_node_) { - return string( + string result = "import thrift = require('thrift');\n" "import Thrift = thrift.Thrift;\n" - "import Q = thrift.Q;\n" - "import Int64 = require('node-int64');\n" + "import Q = thrift.Q;\n"; + if (!gen_bigint_) { + result += "import Int64 = require('node-int64');\n"; + } + result += "import { v4 as uuid } from 'uuid';\n" - "type uuid = string;"); + "type uuid = string;"; + return result; } return string( "import Int64 = require('node-int64');\n" @@ -575,11 +610,14 @@ string t_js_generator::ts_includes() { */ string t_js_generator::ts_service_includes() { if (gen_node_) { - return string( + string result = "import thrift = require('thrift');\n" "import Thrift = thrift.Thrift;\n" - "import Q = thrift.Q;\n" - "import Int64 = require('node-int64');"); + "import Q = thrift.Q;"; + if (!gen_bigint_) { + result += "\nimport Int64 = require('node-int64');"; + } + return result; } return string("import Int64 = require('node-int64');"); } @@ -779,7 +817,10 @@ string t_js_generator::render_const_value(t_type* type, t_const_value* value) { case t_base_type::TYPE_I64: { int64_t const& integer_value = value->get_integer(); - if (integer_value <= max_safe_integer && integer_value >= min_safe_integer) { + if (gen_bigint_) { + // Native BigInt literal — handles the full signed 64-bit range. + out << integer_value << "n"; + } else if (integer_value <= max_safe_integer && integer_value >= min_safe_integer) { out << "new Int64(" << integer_value << ")"; } else { out << "new Int64('" << std::hex << integer_value << std::dec << "')"; @@ -2335,7 +2376,18 @@ void t_js_generator::generate_deserialize_field(ostream& out, } else if (type->is_container()) { generate_deserialize_container(out, type, name); } else if (type->is_base_type() || type->is_enum()) { - indent(out) << name << " = input."; + // In bigint mode the protocol still returns a node-int64 Int64; wrap + // the read site in `thrift.toBigInt(...)` so the assigned value matches + // the generated `bigint` type. gen_bigint_ is only true when gen_node_ + // is true, so the `.value` branch below is never reached in this mode. + bool wrap_i64_bigint = gen_bigint_ && type->is_base_type() && + ((t_base_type*)type)->get_base() == t_base_type::TYPE_I64; + + indent(out) << name << " = "; + if (wrap_i64_bigint) { + out << "thrift.toBigInt("; + } + out << "input."; if (type->is_base_type()) { t_base_type::t_base tbase = ((t_base_type*)type)->get_base(); @@ -2374,6 +2426,10 @@ void t_js_generator::generate_deserialize_field(ostream& out, out << "readI32()"; } + if (wrap_i64_bigint) { + out << ")"; + } + if (!gen_node_) { out << ".value"; } @@ -2565,7 +2621,14 @@ void t_js_generator::generate_serialize_field(ostream& out, t_field* tfield, str out << "writeI32(" << name << ")"; break; case t_base_type::TYPE_I64: - out << "writeI64(" << name << ")"; + // In bigint mode the generated field holds a `bigint`; convert + // back to a node-int64 Int64 before handing to `writeI64`, which + // expects either an Int64 or a Number. + if (gen_bigint_) { + out << "writeI64(thrift.fromBigInt(" << name << "))"; + } else { + out << "writeI64(" << name << ")"; + } break; case t_base_type::TYPE_DOUBLE: out << "writeDouble(" << name << ")"; @@ -2866,7 +2929,7 @@ string t_js_generator::ts_get_type(t_type* type) { ts_type = "number"; break; case t_base_type::TYPE_I64: - ts_type = "Int64"; + ts_type = gen_bigint_ ? "bigint" : "Int64"; break; case t_base_type::TYPE_VOID: ts_type = "void"; diff --git a/lib/nodejs/Makefile.am b/lib/nodejs/Makefile.am index ac192451ad2..4a4547585f5 100644 --- a/lib/nodejs/Makefile.am +++ b/lib/nodejs/Makefile.am @@ -18,7 +18,7 @@ # We call npm twice to work around npm issues stubs: $(top_srcdir)/test/ThriftTest.thrift - $(THRIFT) --gen js:node -o test/ $(top_srcdir)/test/ThriftTest.thrift + $(THRIFT) --gen js:node,bigint=false -o test/ $(top_srcdir)/test/ThriftTest.thrift deps-root: $(top_srcdir)/package.json $(NPM) install $(top_srcdir)/ || $(NPM) install $(top_srcdir)/ diff --git a/lib/nodejs/README.md b/lib/nodejs/README.md index c087440a180..cab82ced125 100644 --- a/lib/nodejs/README.md +++ b/lib/nodejs/README.md @@ -22,7 +22,8 @@ under the License. ## Compatibility -node version 6 or later is required +Node.js 10.18 or later is required, matching the `engines` field in the +package's [`package.json`](../../package.json). ## Install @@ -65,6 +66,48 @@ client.get_slice("Keyspace", "key", new ttypes.ColumnParent({column_family: "Exa Since JavaScript represents all numbers as doubles, int64 values cannot be accurately represented naturally. To solve this, int64 values in responses will be wrapped with Thrift.Int64 objects. The Int64 implementation used is [broofa/node-int64](https://github.com/broofa/node-int64). +### BigInt mode (`js:bigint`, default `true`) + +Native `BigInt` has been available since Node 10.4, so every supported runtime +can use it. The js generator emits BigInt-aware code by default: + +```sh +thrift --gen js:node MyService.thrift # bigint=true is the default +thrift --gen js:node,bigint=false MyService.thrift # legacy node-int64 output +``` + +When `bigint=true` (the default), code generated by `--gen js:node` (and the +`ts`, `es6`, `esm` variants): + +- Emits native `bigint` literals (e.g. `42n`, `9223372036854775807n`) for + int64 constants instead of `new Int64(42)`. +- Drops the `require('node-int64')` / `import Int64 from 'node-int64'` + preamble. +- Declares int64 fields as `bigint` in `.d.ts` files instead of `Int64`. +- Wraps `readI64()` calls with `thrift.toBigInt(...)` so deserialized + fields land in the generated struct as `bigint`, and `writeI64(...)` + calls with `thrift.fromBigInt(...)` so consumers can pass plain + `bigint` values into their structs. + +The flag is only meaningful when `node` is in the option list; for plain +`--gen js` (browser JS) it is silently forced to `false`. + +`TBinaryProtocol` itself is **unchanged** — `readI64` still returns a +`Thrift.Int64` and `writeI64` still expects one. The BigInt boundary lives +entirely in the JS layer, in the two helpers exported from the `thrift` +package: + +```js +const thrift = require("thrift"); + +const big = thrift.toBigInt(prot.readI64()); // Int64 -> bigint +prot.writeI64(thrift.fromBigInt(big)); // bigint -> Int64 +``` + +`thrift.fromBigInt` wraps values to the signed 64-bit range +(`BigInt.asIntN(64, ...)`). Existing code that imports `Thrift.Int64` or +calls `prot.readI64()` directly keeps working with no changes. + ## Client and server examples Several example clients and servers are included in the thrift/lib/nodejs/examples folder and the cross language tutorial thrift/tutorial/nodejs folder. diff --git a/lib/nodejs/lib/thrift/bigint_compat.js b/lib/nodejs/lib/thrift/bigint_compat.js new file mode 100644 index 00000000000..3c8b8aa4953 --- /dev/null +++ b/lib/nodejs/lib/thrift/bigint_compat.js @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Buffer#readBigInt64BE / writeBigInt64BE were added in Node 12.0.0. The +// published package targets `engines: >= 10.18.0`, and browser-side Buffer +// polyfills may also lag, so we feature-detect and fall back to +// readInt32BE / writeUInt32BE plus BigInt arithmetic when the native +// methods are missing. + +const HAS_NATIVE = + typeof Buffer !== "undefined" && + typeof Buffer.prototype.readBigInt64BE === "function" && + typeof Buffer.prototype.writeBigInt64BE === "function"; + +function readBigInt64BEFallback(buf, offset) { + // Signed high 32 bits, unsigned low 32 bits — recombined with a BigInt + // shift+OR. Negative `hi` produces an arbitrarily-negative BigInt with + // ones in all bits above 32, which OR-merges correctly with the + // unsigned low half. + const hi = BigInt(buf.readInt32BE(offset)); + const lo = BigInt(buf.readUInt32BE(offset + 4)); + return (hi << 32n) | lo; +} + +function writeBigInt64BEFallback(buf, value, offset) { + const v = BigInt.asIntN(64, value); + const hi = Number(BigInt.asIntN(32, v >> 32n)); + const lo = Number(BigInt.asUintN(32, v)); + buf.writeInt32BE(hi, offset); + buf.writeUInt32BE(lo, offset + 4); +} + +exports.readBigInt64BE = HAS_NATIVE + ? function (buf, offset) { + return buf.readBigInt64BE(offset); + } + : readBigInt64BEFallback; + +exports.writeBigInt64BE = HAS_NATIVE + ? function (buf, value, offset) { + // Native `writeBigInt64BE` rejects values outside [-2^63, 2^63 - 1]; + // wrap explicitly so callers (e.g. `fromBigInt`) can hand in any + // bigint and get two's-complement truncation, matching the fallback. + buf.writeBigInt64BE(BigInt.asIntN(64, value), offset); + } + : writeBigInt64BEFallback; + +// Exposed so tests can exercise the fallback path on runtimes that have +// the native methods. +exports._readBigInt64BEFallback = readBigInt64BEFallback; +exports._writeBigInt64BEFallback = writeBigInt64BEFallback; +exports._hasNativeBigInt64 = HAS_NATIVE; diff --git a/lib/nodejs/lib/thrift/binary_protocol.js b/lib/nodejs/lib/thrift/binary_protocol.js index b2c3720a6b6..e6c72a56113 100644 --- a/lib/nodejs/lib/thrift/binary_protocol.js +++ b/lib/nodejs/lib/thrift/binary_protocol.js @@ -322,7 +322,7 @@ TBinaryProtocol.prototype.skip = function (type, depth) { if (depth > 64) { throw new Thrift.TProtocolException( Thrift.TProtocolExceptionType.DEPTH_LIMIT, - "Maximum skip depth exceeded" + "Maximum skip depth exceeded", ); } switch (type) { diff --git a/lib/nodejs/lib/thrift/browser.js b/lib/nodejs/lib/thrift/browser.js index 5c843d07137..1a6da7009da 100644 --- a/lib/nodejs/lib/thrift/browser.js +++ b/lib/nodejs/lib/thrift/browser.js @@ -38,6 +38,36 @@ exports.createClient = require("./create_client"); exports.Int64 = require("node-int64"); exports.Q = require("q"); +const bigIntCompat = require("./bigint_compat"); + +/** + * Convert a `node-int64` Int64 to a native `bigint`. Used by code generated + * with `js:bigint=true`. Feature-detects `Buffer#readBigInt64BE` (Node 12+ + * and modern buffer polyfills) and falls back to a `readInt32BE` / + * `readUInt32BE` + BigInt composition when the native method is absent. + * + * @param {Int64} i64 + * @returns {bigint} + */ +exports.toBigInt = function (i64) { + return bigIntCompat.readBigInt64BE(i64.buffer, i64.offset || 0); +}; + +/** + * Convert a native `bigint` to a `node-int64` Int64 for `writeI64`. Values + * outside the signed 64-bit range are wrapped (`BigInt.asIntN(64, ...)`). + * Uses the same feature-detected fallback as `toBigInt`. + * + * @param {bigint} value + * @returns {Int64} + */ +exports.fromBigInt = function (value) { + const Int64 = exports.Int64; + const buf = Buffer.allocUnsafe(8); + bigIntCompat.writeBigInt64BE(buf, value, 0); + return new Int64(buf); +}; + var mpxProtocol = require("./multiplexed_protocol"); exports.Multiplexer = mpxProtocol.Multiplexer; diff --git a/lib/nodejs/lib/thrift/index.js b/lib/nodejs/lib/thrift/index.js index 5f0010f1ed0..d2731b4305d 100644 --- a/lib/nodejs/lib/thrift/index.js +++ b/lib/nodejs/lib/thrift/index.js @@ -58,6 +58,43 @@ exports.createWebServer = web_server.createWebServer; exports.Int64 = require("node-int64"); exports.Q = require("q"); +const bigIntCompat = require("./bigint_compat"); + +/** + * Convert a `node-int64` Int64 (as returned by `TBinaryProtocol.readI64`, + * `TCompactProtocol.readI64`, etc.) to a native `bigint`. Used by code + * generated with `js:bigint=true` to surface int64 values as BigInt without + * a protocol-layer toggle. + * + * Uses `Buffer#readBigInt64BE` when available (Node >= 12), and a + * `readInt32BE` / `readUInt32BE` + BigInt fallback otherwise — so the + * helpers work across the full `engines: >= 10.18.0` support range. + * + * @param {Int64} i64 + * @returns {bigint} + */ +exports.toBigInt = function (i64) { + return bigIntCompat.readBigInt64BE(i64.buffer, i64.offset || 0); +}; + +/** + * Convert a native `bigint` to a `node-int64` Int64 suitable for passing to + * `writeI64`. Values outside the signed 64-bit range are wrapped to fit + * (`BigInt.asIntN(64, ...)`). + * + * Uses `Buffer#writeBigInt64BE` when available, with a 32-bit-pair fallback + * for older runtimes (see `bigint_compat.js`). + * + * @param {bigint} value + * @returns {Int64} + */ +exports.fromBigInt = function (value) { + const Int64 = exports.Int64; + const buf = Buffer.allocUnsafe(8); + bigIntCompat.writeBigInt64BE(buf, value, 0); + return new Int64(buf); +}; + var mpxProcessor = require("./multiplexed_processor"); var mpxProtocol = require("./multiplexed_protocol"); exports.MultiplexedProcessor = mpxProcessor.MultiplexedProcessor; diff --git a/lib/nodejs/test/bigint_helpers.test.js b/lib/nodejs/test/bigint_helpers.test.js new file mode 100644 index 00000000000..c1b91234e60 --- /dev/null +++ b/lib/nodejs/test/bigint_helpers.test.js @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Exercises `thrift.toBigInt` / `thrift.fromBigInt` — the JS-side helpers +// emitted by `js:bigint=true` generated code to convert between node-int64 +// `Int64` and native `bigint`. Verifies they round-trip through the binary +// protocol without any runtime toggle on `TBinaryProtocol`. + +const test = require("tape"); +const thrift = require("thrift"); +const Int64 = require("node-int64"); +const bigIntCompat = require("thrift/lib/nodejs/lib/thrift/bigint_compat"); + +function writeI64(value) { + let buff; + const transport = new thrift.TBufferedTransport(null, function (msg) { + buff = msg; + }); + const prot = new thrift.TBinaryProtocol(transport); + prot.writeI64(value); + prot.flush(); + return buff; +} + +function readI64(serialized) { + const trans = new thrift.TFramedTransport(serialized); + const prot = new thrift.TBinaryProtocol(trans); + return prot.readI64(); +} + +const I64_MAX = (1n << 63n) - 1n; +const I64_MIN = -(1n << 63n); + +test("thrift.fromBigInt / thrift.toBigInt round-trip core values", function (assert) { + for (const v of [0n, 1n, -1n, 42n, -42n, I64_MAX, I64_MIN]) { + const i64 = thrift.fromBigInt(v); + assert.ok(i64 instanceof Int64, `fromBigInt(${v}) returns Int64`); + const back = thrift.toBigInt(i64); + assert.equal( + typeof back, + "bigint", + `toBigInt(...) returns bigint for ${v}`, + ); + assert.equal(back, v, `round-trip ${v}`); + } + assert.end(); +}); + +test("thrift.toBigInt honors a non-zero Int64 offset", function (assert) { + // node-int64 supports `new Int64(buffer, offset)`. + const buf = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42]); + const i64 = new Int64(buf, 8); + assert.equal(thrift.toBigInt(i64), 42n, "offset is honored"); + assert.end(); +}); + +test("thrift.fromBigInt wraps to the 64-bit signed range", function (assert) { + // Anything outside [-2^63, 2^63 - 1] is wrapped via BigInt.asIntN(64, ...). + const overflow = 1n << 63n; // 2^63 — wraps to -2^63 + const i64 = thrift.fromBigInt(overflow); + assert.equal(thrift.toBigInt(i64), I64_MIN, "2^63 wraps to MIN_I64"); + assert.end(); +}); + +test("bigint values round-trip through TBinaryProtocol via the helpers", function (assert) { + for (const v of [0n, 1n, -1n, 42n, -42n, I64_MAX, I64_MIN]) { + const wire = writeI64(thrift.fromBigInt(v)); + const result = thrift.toBigInt(readI64(wire)); + assert.equal(result, v, `protocol round-trip for ${v}`); + } + assert.end(); +}); + +test("values beyond Number.MAX_SAFE_INTEGER survive the protocol round-trip", function (assert) { + const big = (1n << 53n) + 1n; + const back = thrift.toBigInt(readI64(writeI64(thrift.fromBigInt(big)))); + assert.equal(back, big, "above MAX_SAFE_INTEGER"); + + const small = -((1n << 53n) + 1n); + const backNeg = thrift.toBigInt(readI64(writeI64(thrift.fromBigInt(small)))); + assert.equal(backNeg, small, "below MIN_SAFE_INTEGER"); + assert.end(); +}); + +// --- bigint_compat fallback --------------------------------------------- +// +// `toBigInt` / `fromBigInt` use `Buffer#readBigInt64BE` / `writeBigInt64BE` +// when available and fall back to a `readInt32BE` / `writeUInt32BE` + BigInt +// composition otherwise (needed for Node 10.x and older Buffer polyfills, +// since the native methods landed in Node 12). The fallback path is exposed +// for direct testing so it's exercised even on runtimes that have the +// natives. + +test("readBigInt64BE fallback matches native across boundary values", function (assert) { + const cases = [ + [0n, [0, 0, 0, 0, 0, 0, 0, 0]], + [1n, [0, 0, 0, 0, 0, 0, 0, 1]], + [-1n, [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]], + [42n, [0, 0, 0, 0, 0, 0, 0, 42]], + [-42n, [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd6]], + [I64_MAX, [0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]], + [I64_MIN, [0x80, 0, 0, 0, 0, 0, 0, 0]], + ]; + for (const [expected, bytes] of cases) { + const buf = Buffer.from(bytes); + assert.equal( + bigIntCompat._readBigInt64BEFallback(buf, 0), + expected, + `read fallback for ${expected}`, + ); + } + assert.end(); +}); + +test("writeBigInt64BE fallback matches native across boundary values", function (assert) { + const cases = [ + [0n, [0, 0, 0, 0, 0, 0, 0, 0]], + [1n, [0, 0, 0, 0, 0, 0, 0, 1]], + [-1n, [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]], + [42n, [0, 0, 0, 0, 0, 0, 0, 42]], + [-42n, [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd6]], + [I64_MAX, [0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]], + [I64_MIN, [0x80, 0, 0, 0, 0, 0, 0, 0]], + ]; + for (const [value, expected] of cases) { + const buf = Buffer.alloc(8); + bigIntCompat._writeBigInt64BEFallback(buf, value, 0); + assert.deepEqual(Array.from(buf), expected, `write fallback for ${value}`); + } + assert.end(); +}); + +test("fallback round-trips at non-zero offsets", function (assert) { + const buf = Buffer.alloc(16); + bigIntCompat._writeBigInt64BEFallback(buf, I64_MAX, 4); + assert.equal(bigIntCompat._readBigInt64BEFallback(buf, 4), I64_MAX); + bigIntCompat._writeBigInt64BEFallback(buf, I64_MIN, 8); + assert.equal(bigIntCompat._readBigInt64BEFallback(buf, 8), I64_MIN); + assert.end(); +}); diff --git a/lib/nodejs/test/int64_bigint.test.js b/lib/nodejs/test/int64_bigint.test.js new file mode 100644 index 00000000000..91fb5739f00 --- /dev/null +++ b/lib/nodejs/test/int64_bigint.test.js @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Exercises the `js:bigint=true` default of the compiler. The fixture is +// regenerated by testAll.sh into `gen-nodejs-bigint/` using the bare +// `--gen js:node,es6` flag set (bigint=true is implicit). + +const test = require("tape"); +const i64types = require("./gen-nodejs-bigint/Int64Test_types.js"); + +test("js:bigint=true emits native BigInt constants", function (assert) { + assert.equal(typeof i64types.SMALL_INT64, "bigint", "scalar is bigint"); + assert.equal(i64types.SMALL_INT64, 42n, "SMALL_INT64"); + assert.equal( + i64types.MAX_JS_SAFE_INT64, + BigInt(Number.MAX_SAFE_INTEGER), + "MAX_JS_SAFE_INT64", + ); + assert.equal( + i64types.MIN_JS_SAFE_INT64, + BigInt(Number.MIN_SAFE_INTEGER), + "MIN_JS_SAFE_INT64", + ); + assert.equal( + i64types.MAX_JS_SAFE_PLUS_ONE_INT64, + BigInt(Number.MAX_SAFE_INTEGER) + 1n, + "MAX_JS_SAFE_PLUS_ONE_INT64", + ); + assert.equal( + i64types.MIN_JS_SAFE_MINUS_ONE_INT64, + BigInt(Number.MIN_SAFE_INTEGER) - 1n, + "MIN_JS_SAFE_MINUS_ONE_INT64", + ); + assert.equal( + i64types.MAX_SIGNED_INT64, + (1n << 63n) - 1n, + "MAX_SIGNED_INT64 — full 64-bit range", + ); + assert.equal( + i64types.MIN_SIGNED_INT64, + -(1n << 63n), + "MIN_SIGNED_INT64 — full 64-bit range", + ); + assert.end(); +}); + +test("js:bigint=true emits BigInt list elements", function (assert) { + const expected = [ + 42n, + BigInt(Number.MAX_SAFE_INTEGER), + BigInt(Number.MIN_SAFE_INTEGER), + BigInt(Number.MAX_SAFE_INTEGER) + 1n, + BigInt(Number.MIN_SAFE_INTEGER) - 1n, + (1n << 63n) - 1n, + -(1n << 63n), + ]; + assert.equal(i64types.INT64_LIST.length, expected.length, "list length"); + for (let i = 0; i < expected.length; ++i) { + assert.equal(typeof i64types.INT64_LIST[i], "bigint", `INT64_LIST[${i}]`); + assert.equal(i64types.INT64_LIST[i], expected[i], `INT64_LIST[${i}] value`); + } + assert.end(); +}); + +test("js:bigint=true generated map keys are decimal strings, values are BigInt", function (assert) { + // Map keys cannot be BigInt in JS object literals — the generator stringifies + // them. Values should be BigInt under bigint=true. + const map = i64types.INT64_2_INT64_MAP; + assert.equal(typeof map["42"], "bigint", "value at key '42' is bigint"); + assert.equal(map["42"], 42n, "value at key '42' equals 42n"); + assert.equal( + map[String((1n << 63n) - 1n)], + (1n << 63n) - 1n, + "value at MAX_SIGNED_INT64 key round-trips", + ); + assert.end(); +}); + +test("bigint-mode generated module does not import node-int64", function (assert) { + const fs = require("fs"); + const path = require("path"); + const src = fs.readFileSync( + path.join(__dirname, "gen-nodejs-bigint", "Int64Test_types.js"), + "utf8", + ); + assert.notOk( + /require\(['"]node-int64['"]\)/.test(src), + "no `require('node-int64')` in bigint-mode output", + ); + assert.notOk( + /new Int64\(/.test(src), + "no `new Int64(...)` in bigint-mode output", + ); + assert.end(); +}); diff --git a/lib/nodejs/test/testAll.sh b/lib/nodejs/test/testAll.sh index ce3c2520007..ebc58f19cb4 100755 --- a/lib/nodejs/test/testAll.sh +++ b/lib/nodejs/test/testAll.sh @@ -87,28 +87,38 @@ testEpisodicCompilation() TESTOK=0 # generating Thrift code - -${THRIFT_COMPILER} -o ${DIR} --gen js:node ${THRIFT_FILES_DIR}/ThriftTest.thrift -${THRIFT_COMPILER} -o ${DIR} --gen js:node ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift -${THRIFT_COMPILER} -o ${DIR} --gen js:node ${THRIFT_FILES_DIR}/Int64Test.thrift -${THRIFT_COMPILER} -o ${DIR} --gen js:node ${THRIFT_FILES_DIR}/Include.thrift +# +# The js generator defaults to `bigint=true`, which emits native BigInt +# literals and drops the node-int64 import. The existing test suite still +# uses node-int64 semantics in its assertions (see int64.test.js, +# test_driver.mjs, test-cases.mjs), so we opt out here. Bigint-mode +# codegen is exercised separately under gen-nodejs-bigint. + +${THRIFT_COMPILER} -o ${DIR} --gen js:node,bigint=false ${THRIFT_FILES_DIR}/ThriftTest.thrift +${THRIFT_COMPILER} -o ${DIR} --gen js:node,bigint=false ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift +${THRIFT_COMPILER} -o ${DIR} --gen js:node,bigint=false ${THRIFT_FILES_DIR}/Int64Test.thrift +${THRIFT_COMPILER} -o ${DIR} --gen js:node,bigint=false ${THRIFT_FILES_DIR}/Include.thrift mkdir ${DIR}/gen-nodejs-es6 -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${THRIFT_FILES_DIR}/ThriftTest.thrift -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${THRIFT_FILES_DIR}/Int64Test.thrift -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${THRIFT_FILES_DIR}/Include.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6,bigint=false ${THRIFT_FILES_DIR}/ThriftTest.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6,bigint=false ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6,bigint=false ${THRIFT_FILES_DIR}/Int64Test.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6,bigint=false ${THRIFT_FILES_DIR}/Include.thrift mkdir ${DIR}/gen-nodejs-esm -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm ${THRIFT_FILES_DIR}/ThriftTest.thrift -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm ${THRIFT_FILES_DIR}/Int64Test.thrift -${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm ${THRIFT_FILES_DIR}/Include.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm,bigint=false ${THRIFT_FILES_DIR}/ThriftTest.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm,bigint=false ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm,bigint=false ${THRIFT_FILES_DIR}/Int64Test.thrift +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-esm --gen js:node,es6,esm,bigint=false ${THRIFT_FILES_DIR}/Include.thrift + +# Bigint-mode codegen — only Int64Test.thrift is exercised by int64_bigint.test.js. +mkdir ${DIR}/gen-nodejs-bigint +${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-bigint --gen js:node,es6 ${THRIFT_FILES_DIR}/Int64Test.thrift # generate episodic compilation test code TYPES_PACKAGE=${EPISODIC_DIR}/node_modules/types-package # generate the first episode mkdir --parents ${EPISODIC_DIR}/gen-1/first-episode -${THRIFT_COMPILER} -o ${EPISODIC_DIR}/gen-1/first-episode --gen js:node,ts,thrift_package_output_directory=first-episode ${THRIFT_FILES_DIR}/Types.thrift +${THRIFT_COMPILER} -o ${EPISODIC_DIR}/gen-1/first-episode --gen js:node,ts,bigint=false,thrift_package_output_directory=first-episode ${THRIFT_FILES_DIR}/Types.thrift # create a "package" from the first episode and "install" it, the episode file must be at the module root mkdir --parents ${TYPES_PACKAGE}/first-episode @@ -122,7 +132,7 @@ rm --force --recursive ${EPISODIC_DIR}/gen-1 # generate the second episode mkdir --parents ${EPISODIC_DIR}/gen-2/second-episode -${THRIFT_COMPILER} -o ${EPISODIC_DIR}/gen-2/second-episode --gen js:node,ts,imports=${TYPES_PACKAGE} ${THRIFT_FILES_DIR}/Service.thrift +${THRIFT_COMPILER} -o ${EPISODIC_DIR}/gen-2/second-episode --gen js:node,ts,bigint=false,imports=${TYPES_PACKAGE} ${THRIFT_FILES_DIR}/Service.thrift if [ -f ${EPISODIC_DIR}/gen-2/second-episode/Types_types.js ]; then TESTOK=1 fi @@ -130,9 +140,11 @@ fi # unit tests node ${DIR}/binary.test.js || TESTOK=1 +node ${DIR}/bigint_helpers.test.js || TESTOK=1 node ${DIR}/check_set_uniqueness.test.js || TESTOK=1 node ${DIR}/header.test.js || TESTOK=1 node ${DIR}/int64.test.js || TESTOK=1 +node ${DIR}/int64_bigint.test.js || TESTOK=1 node ${DIR}/deep-constructor.test.js || TESTOK=1 node ${DIR}/generated-exceptions.test.js || TESTOK=1 node ${DIR}/include.test.mjs || TESTOK=1 diff --git a/lib/nodets/Makefile.am b/lib/nodets/Makefile.am index de523f5f42f..41a7d331992 100644 --- a/lib/nodets/Makefile.am +++ b/lib/nodets/Makefile.am @@ -19,8 +19,8 @@ stubs: $(top_srcdir)/test/ThriftTest.thrift mkdir -p test-compiled - $(THRIFT) --gen js:node,ts -o test/ $(top_srcdir)/test/ThriftTest.thrift && $(THRIFT) --gen js:node,ts -o test-compiled $(top_srcdir)/test/ThriftTest.thrift - $(THRIFT) --gen js:node,ts -o test/ $(top_srcdir)/test/Int64Test.thrift && $(THRIFT) --gen js:node,ts -o test-compiled $(top_srcdir)/test/Int64Test.thrift + $(THRIFT) --gen js:node,ts,bigint=false -o test/ $(top_srcdir)/test/ThriftTest.thrift && $(THRIFT) --gen js:node,ts,bigint=false -o test-compiled $(top_srcdir)/test/ThriftTest.thrift + $(THRIFT) --gen js:node,ts,bigint=false -o test/ $(top_srcdir)/test/Int64Test.thrift && $(THRIFT) --gen js:node,ts,bigint=false -o test-compiled $(top_srcdir)/test/Int64Test.thrift ts-compile: stubs mkdir -p test-compiled diff --git a/lib/nodets/test/runClient.sh b/lib/nodets/test/runClient.sh index 8d5e9a33f00..1a398142e97 100755 --- a/lib/nodets/test/runClient.sh +++ b/lib/nodets/test/runClient.sh @@ -10,8 +10,8 @@ export NODE_PATH="${DIR}:${DIR}/../../nodejs/lib:${NODE_PATH}" compile() { #generating thrift code - ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts ${DIR}/../../../test/ThriftTest.thrift - ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts ${DIR}/../../../test/ThriftTest.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/ThriftTest.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/ThriftTest.thrift } compile diff --git a/lib/nodets/test/runServer.sh b/lib/nodets/test/runServer.sh index 4eee92717ec..dbc4ba641a8 100755 --- a/lib/nodets/test/runServer.sh +++ b/lib/nodets/test/runServer.sh @@ -10,8 +10,8 @@ export NODE_PATH="${DIR}:${DIR}/../../nodejs/lib:${NODE_PATH}" compile() { #generating thrift code - ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts ${DIR}/../../../test/ThriftTest.thrift - ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts ${DIR}/../../../test/ThriftTest.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/ThriftTest.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/ThriftTest.thrift } compile diff --git a/lib/nodets/test/testAll.sh b/lib/nodets/test/testAll.sh index 3be12c3622a..5fe810b3b7a 100755 --- a/lib/nodets/test/testAll.sh +++ b/lib/nodets/test/testAll.sh @@ -10,10 +10,10 @@ export NODE_PATH="${DIR}:${DIR}/../../nodejs/lib:${NODE_PATH}" compile() { #generating thrift code - ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts ${DIR}/../../../test/ThriftTest.thrift - ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts ${DIR}/../../../test/Int64Test.thrift - ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts ${DIR}/../../../test/ThriftTest.thrift - ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts ${DIR}/../../../test/Int64Test.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/ThriftTest.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/Int64Test.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/ThriftTest.thrift + ${DIR}/../../../compiler/cpp/thrift -o ${COMPILEDDIR} --gen js:node,ts,bigint=false ${DIR}/../../../test/Int64Test.thrift tsc --outDir $COMPILEDDIR --project $DIR/tsconfig.json }