From 1f24b29ed7ba4a04f73a0cfeea90d8797ae8d1f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 02:38:52 +0000 Subject: [PATCH 1/6] [deps] upgrade Math.js to 15.2.0 and preserve expression contracts Math.js 9.5.0 -> 15.2.0 (fraction.js 5 with bigint, typed-function 4); 15.2.0 fixes the parser vulnerabilities of 13.1-15.1 (CVE-2026-40897, CVE-2026-41139, GHSA-jvff-x2qm-6286). Engine adaptations to keep the 0.1.17 contract: add/subtract/multiply/ divide round results with a denominator above 2^53 - 1 to the closest fraction with a smaller one (long sums such as the probabilistic sensitivity analysis mean stayed bounded with fraction.js 4; with bigint they grew to thousands of digits and toFloat returned NaN); toFloat handles Fractions beyond the double range; eval with asNumber rejects Unit results as before. Tests: the contract of built artifacts of a release is selected explicitly (SD_EXPRESSION_ENGINE_BASELINE=0.1.17, npm run test:baseline for the committed dist/), version-dependent expectations assert both Math.js versions. The bundles parse as ES2020 (Math.js 13+ requires ES2020 and BigInt); the package sources still compile to ES5. --- .github/workflows/ci.yml | 15 ++- CHANGELOG.md | 27 ++++++ README.md | 44 ++++++--- package-lock.json | 86 ++++++++++------- package.json | 6 +- rollup.config.mjs | 12 ++- scripts/check-build.js | 21 +++-- src/expression-engine.js | 72 ++++++++++++++- test/contract/exports.test.js | 9 +- test/contract/load.js | 21 +++++ test/contract/mathjs-version.test.js | 132 +++++++++++++++++++++++++++ test/contract/numbers.test.js | 9 +- test/contract/run.js | 19 +++- test/contract/serialization.test.js | 5 +- 14 files changed, 403 insertions(+), 75 deletions(-) create mode 100644 test/contract/mathjs-version.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77c3d74..ae0360f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,9 +33,12 @@ jobs: - name: Install dependencies run: npm ci --no-audit --no-fund - # The committed dist/ is the 0.1.17 release build (gulp/browserify): the - # contract tests and the Jasmine specs must keep passing on it - - name: Tests on the committed dist (0.1.17 build) and the sources + # The committed dist/ is the 0.1.17 release build (gulp/browserify, Math.js + # 9.5.0): tested against the contract of that release + - name: Committed dist against the 0.1.17 contract + run: npm run test:baseline + + - name: Sources (browserify + the package transform) and Jasmine specs run: npm test # Isolated copy: npm ci, build, artifacts (ES5, source maps), tests on the @@ -71,7 +74,11 @@ jobs: run: npm run build - name: Tests on the fresh dist (standalone and module bundles, plain and minified), the sources and the Jasmine specs - run: npm test + shell: bash + run: | + set -e + npm run test:dist + npm test - name: The build changed nothing outside dist/ shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index f34c8fb..9aae475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## Unreleased +### Dependencies: Math.js 15.2.0 (from 9.5.0) + +- **Runtime requirements**: Math.js 13+ requires ES2020 and `BigInt` (Chrome and + Edge 80+, Firefox 74+, Safari 14+; no Internet Explorer). The bundles contain + Math.js as published (ES2020 syntax); the package sources still compile to ES5. +- Fractions use fraction.js 5 (`bigint`). The arithmetic helpers (`add`, + `subtract`, `multiply`, `divide`) round a result whose denominator exceeds + 2^53 - 1 to the closest fraction with a smaller denominator, so long sums (the + mean of a probabilistic sensitivity analysis) stay fast and finite as with + Math.js 9 (without it they grew to thousands of digits and `toFloat` returned + `NaN`); `toFloat` handles Fractions beyond the double range. +- `eval(expr, true)` rejects a `Unit` result as before (Math.js 14+ would convert + the value of the unit). +- The JSON form of a Fraction (`toJSON`, data model DTO of the job repository) + writes `n` and `d` as strings; the 0.1.17 form with numbers is still read. + Saved diagrams are unchanged (Fractions are saved as fraction strings). +- Behaviour of Math.js for user expressions (Math.js 10-15 changelogs, checked + against the expressions of saved diagrams, which give the same values): + `-1 / 2 x` is `(-1/2) * x` (was `-1 / (2 x)`); element-wise `sqrt`, `exp`, + `sin` etc. of matrices throw (use `map`); `mod` accepts negative divisors; + `cond ? : b` is an error; `size()` returns an array; `x[i, :]` returns a + vector; integers in numeric strings beyond 2^53 keep all digits; new syntax + `??`, `?.` and new functions (`isFinite`, `num`, `den`, `bernoulli`, ...). +- Security: Math.js 15.2.0 fixes GHSA-jvff-x2qm-6286, GHSA-29qv-4j9f-fjw5 + (CVE-2026-40897) and GHSA-5v89-rwgr-qj6g (CVE-2026-41139), which affect + 13.1.0 - 15.1.x (not 9.5.0). + ### Build - The `dist/` bundles are built with Rollup and npm scripts instead of diff --git a/README.md b/README.md index 1046fb5..0784edc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # sd-expression-engine SilverDecisions expression engine module: evaluation of the expressions of -SilverDecisions diagrams with [Math.js](https://mathjs.org/) 9.5.0, exact +SilverDecisions diagrams with [Math.js](https://mathjs.org/) 15.2.0, exact Fraction arithmetic and the sd-random distributions. [![CI](https://github.com/SilverDecisions/sd-expression-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/SilverDecisions/sd-expression-engine/actions/workflows/ci.yml) @@ -29,15 +29,22 @@ Built files (`dist/`, committed, rebuilt by the release): The plain files carry an inline source map, the minified ones a `.map` file. +Runtime requirements: Math.js 13+ requires ES2020 and `BigInt` (Chrome and Edge 80+, +Firefox 74+, Safari 14+, no Internet Explorer). The package sources compile to +ES5; the bundles and consumer bundles contain Math.js as published (ES2020). +Releases up to 0.1.17 (Math.js 9.5.0) ran on ES5 engines. + ## Development Node 24 (`.nvmrc`); CI runs Node 22 and 24 on Linux and a build smoke test on -Windows. Math.js is pinned to exactly 9.5.0. +Windows. Math.js is pinned to an exact version (15.2.0). ```bash npm ci npm run build # dist/ with Rollup (rollup.config.mjs), ~2 min (terser on the large bundles) -npm test # contract tests on dist/ and the sources, Jasmine specs in Node +npm test # contract tests on the sources (browserify + transform), Jasmine specs in Node +npm run test:dist # contract tests on the four bundles in dist/ (after npm run build) +npm run test:baseline # the committed dist/ (0.1.17 build) against the 0.1.17 contract npm run test:build # isolated build check, see scripts/check-build.js ``` @@ -49,8 +56,8 @@ and sd-random are compiled first by `scripts/browserify-transform.mjs` with the Babel 7 options of their own `browserify.transform` field and CommonJS modules, exactly as babelify compiles them for consumers, so the module interop (`src/mathjs.js` sets `module.exports`, `import * as math`) is Babel's. -Compilation target: ES5 syntax (`@babel/preset-env` without targets, no -browserslist config); class fields are assignments (Babel 6 +Compilation target of the package sources: ES5 syntax (`@babel/preset-env` +without targets, no browserslist config); class fields are assignments (Babel 6 `transform-class-properties`). Math.js (`lib/cjs`, through its `require` export condition, as browserify resolves `main`) and its dependencies are bundled as published, not compiled; the `browser` fields are honoured @@ -59,8 +66,8 @@ for compatibility with the consumers' transform; a migration of that configuration (and Babel 8) is a separate task. `npm run test:build` (`scripts/check-build.js`): copy of the working tree, -`npm ci`, build without warnings, the six artifacts exist and the bundles are -ES5, source maps are valid JSON with `sources`/`sourcesContent` and map a known +`npm ci`, build without warnings, the six artifacts exist and the bundles parse +as ES2020 (the package sources compiled by a consumer as ES5), source maps are valid JSON with `sources`/`sourcesContent` and map a known statement back to `src/expression-engine.js`, nothing changed outside `dist/`, contract tests on the four fresh bundles and the sources, Jasmine specs, `npm pack` installed into an empty browserify 17 + babelify 10 consumer (contract @@ -86,19 +93,28 @@ artifact, chosen with `SD_EXPRESSION_ENGINE_TARGET`: | a file path | a CommonJS module exporting the package namespace | `SD_EXPRESSION_ENGINE_DIST=` tests the bundles of another `dist/` directory. +`SD_EXPRESSION_ENGINE_BASELINE=` (`0.1.17`) tests built artifacts of +that release against its contract: the Math.js version it contains (9.5.0) and +the version-dependent expectations; without it the current contract applies +(Math.js of `package.json`) and a bundle with another Math.js version fails. ```bash -npm test # all five targets on ./dist and the sources, then Jasmine +npm test # the sources (target source), then Jasmine +node test/contract/run.js # all five targets on ./dist and the sources +npm run test:baseline # committed dist/ against the 0.1.17 contract SD_EXPRESSION_ENGINE_TARGET=module node --test test/contract/ # one target node test/contract/run.js --targets=module,source node test/contract/run.js --dist= # another dist/ directory -npm run test:published # the bundles of sd-expression-engine@0.1.17 from npm +npm run test:published # the bundles of sd-expression-engine@0.1.17 from npm (0.1.17 contract) npx jasmine --config=test/jasmine.json # the Jasmine specs (test/specs) in Node ``` -The committed `dist/` is the 0.1.17 release build (gulp/browserify): `npm test` -on a fresh checkout tests it (CI does so as well), `npm run test:build` tests a -fresh build. The Jasmine specs compile the sources with `@babel/register` +The committed `dist/` is the 0.1.17 release build (gulp/browserify, Math.js +9.5.0) until the next release: `npm run test:baseline` tests it against the +0.1.17 contract (CI does so as well), `npm test` the current sources and +`npm run test:build` a fresh build. Tests whose expectation depends on the Math.js +version (`test/contract/mathjs-version.test.js`, `[version difference]`) assert +both versions explicitly. The Jasmine specs compile the sources with `@babel/register` (`test/jasmine-babel.js`) and run in Node; the bundles are not run in a real browser or Web Worker by these tests (see the integration notes of the PRs). @@ -129,7 +145,9 @@ pass while the defect is present and fail once it is fixed. `eval(expr, false)` reject them. - `serialize(±Infinity)` and `round(±Infinity)` throw; `add` / `divide` of `Infinity` and a finite value throw (implicit conversion to Fraction). -- `median` and `std` do not accept Fractions (Math.js 9, `number` config). +- `median` and `std` do not accept Fractions (`number` config). +- An expression can change the shared Math.js configuration for all later + evaluations (`config({number: "BigNumber"})`). - `isExpressionObject` is false for a live Fraction; the replacer works inside `JSON.stringify`, which calls `toJSON()` first. - The Math.js instance is shared: `createUnit` in an expression changes it for diff --git a/package-lock.json b/package-lock.json index 6f40f86..902b6c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.17", "license": "LGPL-3.0", "dependencies": { - "mathjs": "9.5.0", + "mathjs": "15.2.0", "sd-random": "^0.1.4", "sd-utils": "^0.1.15" }, @@ -1537,18 +1537,14 @@ } }, "node_modules/@babel/runtime": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", - "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", - "dependencies": { - "regenerator-runtime": "^0.13.4" + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -3820,9 +3816,17 @@ "license": "MIT" }, "node_modules/complex.js": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.15.tgz", - "integrity": "sha512-gDBvQU8IG139ZBQTSo2qvDFP+lANMGluM779csXOr6ny1NUtA3wkUnCFjlDNH/moAVfXtvClYt6G0zarFbtz5w==" + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.3.tgz", + "integrity": "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -4140,9 +4144,10 @@ } }, "node_modules/decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" }, "node_modules/decode-uri-component": { "version": "0.2.2", @@ -5024,9 +5029,17 @@ } }, "node_modules/fraction.js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", - "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==" + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } }, "node_modules/fs-extra": { "version": "8.1.0", @@ -7107,19 +7120,26 @@ } }, "node_modules/mathjs": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-9.5.0.tgz", - "integrity": "sha512-a6QFEoLFLEI1cKr0B7lwAM1TTNAfNTxppimyqS5WhxGKXbV59DK4OX606z05PpuKpTiq7gTtPiSV0OAwFfxSvw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-15.2.0.tgz", + "integrity": "sha512-UAQzSVob9rNLdGpqcFMYmSu9dkuLYy7Lr2hBEQS5SHQdknA9VppJz3cy2KkpMzTODunad6V6cNv+5kOLsePLow==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.15.4", - "complex.js": "^2.0.15", - "decimal.js": "^10.3.1", + "@babel/runtime": "^7.26.10", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", - "fraction.js": "^4.1.1", + "fraction.js": "^5.2.1", "javascript-natural-sort": "^0.7.1", "seedrandom": "^3.0.5", "tiny-emitter": "^2.1.0", - "typed-function": "^2.0.0" + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" } }, "node_modules/md5.js": { @@ -9944,9 +9964,13 @@ } }, "node_modules/typed-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz", - "integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==" + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.2.tgz", + "integrity": "sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==", + "license": "MIT", + "engines": { + "node": ">= 18" + } }, "node_modules/typedarray": { "version": "0.0.6", diff --git a/package.json b/package.json index 7daeaa8..b8652c9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "clean": "node scripts/clean.js", "build": "npm run clean && rollup -c", - "test": "node test/contract/run.js && jasmine --config=test/jasmine.json", + "test": "node test/contract/run.js --targets=source && jasmine --config=test/jasmine.json", + "test:dist": "node test/contract/run.js --targets=standalone,standalone.min,module,module.min", + "test:baseline": "node test/contract/run.js --baseline=0.1.17", "test:published": "node test/contract/run.js --published=0.1.17", "test:build": "node scripts/check-build.js", "release": "release-it --verbose" @@ -69,7 +71,7 @@ "rollup": "^4.67.3" }, "dependencies": { - "mathjs": "9.5.0", + "mathjs": "15.2.0", "sd-utils": "^0.1.15", "sd-random": "^0.1.4" } diff --git a/rollup.config.mjs b/rollup.config.mjs index 5390f6c..37dd82a 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -41,10 +41,16 @@ const plugins = () => [ ]; // Math.js lib/cjs has a few `/* #__PURE__ */` comments Rollup cannot place -// (INVALID_ANNOTATION, the comment is dropped, the code is unchanged); every -// other warning is reported. +// (INVALID_ANNOTATION, the comment is dropped, the code is unchanged) and, from +// Math.js 15, circular requires among its own utils (object -> is -> map -> +// customs), which CommonJS (strictRequires) resolves as Node does; every other +// warning is reported. +const MATHJS = /[\\/]node_modules[\\/]mathjs[\\/]/; function onwarn(warning, warn) { - if (warning.code === 'INVALID_ANNOTATION' && /[\\/]node_modules[\\/]mathjs[\\/]/.test(warning.id || '')) { + if (warning.code === 'INVALID_ANNOTATION' && MATHJS.test(warning.id || '')) { + return; + } + if (warning.code === 'CIRCULAR_DEPENDENCY' && (warning.ids || []).length && warning.ids.every(id => MATHJS.test(id))) { return; } warn(warning); diff --git a/scripts/check-build.js b/scripts/check-build.js index 95cd515..cf1ad48 100644 --- a/scripts/check-build.js +++ b/scripts/check-build.js @@ -3,7 +3,9 @@ // // 1. copy the files of the working tree (git ls-files) to a temp dir, `npm ci` there // 2. `npm run build`: exit code 0, no Rollup warnings -// 3. all dist artifacts exist, nothing else is in dist/; the bundles are ES5; +// 3. all dist artifacts exist, nothing else is in dist/; the bundles parse as +// ES2020 (BUNDLE_ECMA_VERSION: Math.js 13+ requires ES2020 and bigint and is +// bundled as published; the package's own sources are checked for ES5 in 6); // their source maps (inline in the plain files, .map files next to the // minified ones) are version 3 JSON with sources and sourcesContent, and the // position of a known statement of src/expression-engine.js maps back to @@ -54,6 +56,9 @@ const CONSUMER_DEPS = ['browserify@17', 'babelify@10', '@babel/core@7', '@babel/ 'babel-plugin-transform-class-properties@6', 'babel-plugin-transform-object-assign@6', 'babel-plugin-transform-builtin-extend@1']; const TARGETS = ['standalone', 'standalone.min', 'module', 'module.min']; +// syntax level of the dist bundles (Math.js 15 lib/cjs uses ES2020: bigint +// literals, optional chaining); the package sources compile to ES5 +const BUNDLE_ECMA_VERSION = 2020; const args = process.argv.slice(2); const flag = name => args.includes(name); @@ -120,10 +125,10 @@ function copyTree(dest) { // acorn from the isolated copy (installed by its `npm ci`) let toolsDir = root; -function isES5(file) { +function isES5(file, ecmaVersion) { const acorn = require(require.resolve('acorn', {paths: [toolsDir]})); try { - acorn.parse(fs.readFileSync(file, 'utf8'), {ecmaVersion: 5}); + acorn.parse(fs.readFileSync(file, 'utf8'), {ecmaVersion: ecmaVersion || 5}); return null; } catch (e) { return e.message; @@ -305,8 +310,8 @@ function main() { continue; } if (a.endsWith('.js')) { - const err = isES5(f); - err ? fail(a + ' is not ES5: ' + err) : null; + const err = isES5(f, BUNDLE_ECMA_VERSION); + err ? fail(a + ' is not ES' + BUNDLE_ECMA_VERSION + ': ' + err) : null; const problem = sourceMapProblem(f, a.endsWith('.min.js'), copy); problem ? fail(a + ': ' + problem) : null; } @@ -315,7 +320,7 @@ function main() { .map(f => 'dist/' + String(f).split(path.sep).join('/')) .filter(f => fs.statSync(path.join(copy, f)).isFile() && !ARTIFACTS.includes(f)); extra.length ? fail('unexpected files in dist/: ' + extra.join(', ')) : null; - ok(ARTIFACTS.length + ' artifacts checked (ES5, source maps: JSON, sources, sourcesContent, mapping of ' + ERROR_FILE + '); sizes: ' + + ok(ARTIFACTS.length + ' artifacts checked (ES' + BUNDLE_ECMA_VERSION + ', source maps: JSON, sources, sourcesContent, mapping of ' + ERROR_FILE + '); sizes: ' + ARTIFACTS.filter(a => fs.existsSync(path.join(copy, a))).map(a => path.basename(path.dirname(a)) + '/' + path.basename(a) + ' ' + Math.round(fs.statSync(path.join(copy, a)).size / 1024) + ' KiB').join(', ')); @@ -422,7 +427,9 @@ function checkPackage(tmp, copy) { return false; } }).map(n => n + '@' + require(path.join(modules, n, 'package.json')).version); - console.log('info full consumer bundle is not ES5 (' + err + '): fresh Math.js dependencies ' + (notES5.join(', ') || '?')); + console.log('info full consumer bundle is not ES5 (' + err + '): Math.js ' + installed('mathjs') + ' and its dependencies ' + (notES5.join(', ') || '?')); + const err2020 = isES5(bundle, BUNDLE_ECMA_VERSION); + err2020 ? fail('consumer bundle is not ES' + BUNDLE_ECMA_VERSION + ': ' + err2020) : ok('consumer bundle parses as ES' + BUNDLE_ECMA_VERSION); } else { ok('browserify bundle of require(\'' + NAME + '\') is ES5'); } diff --git a/src/expression-engine.js b/src/expression-engine.js index 59d3dac..ad8db51 100644 --- a/src/expression-engine.js +++ b/src/expression-engine.js @@ -13,6 +13,63 @@ sdRandom.functionNameList.forEach(fnName => { } }); +// Math.js 14+ (fraction.js 5) computes Fractions exactly with bigint numerators +// and denominators: a sum of many evaluated (float) values, e.g. the expected +// values of the probabilistic sensitivity analysis, grows without limit, gets +// slow and its JS number (valueOf) becomes NaN. With Math.js 9 (fraction.js 4, +// JS numbers) Fractions stayed within the safe integer range. The arithmetic +// helpers therefore round a result whose denominator exceeds MAX_DENOMINATOR to +// the closest fraction with a denominator up to MAX_DENOMINATOR. +const MAX_DENOMINATOR = BigInt(Number.MAX_SAFE_INTEGER); +const ZERO = BigInt(0); +const ONE = BigInt(1); + +function abs(v) { + return v < ZERO ? -v : v; +} + +// best rational approximation with a bounded denominator (continued fractions) +function limitDenominator(f) { + if (!math.isFraction(f) || f.d <= MAX_DENOMINATOR) { + return f; + } + let p0 = ZERO, q0 = ONE, p1 = ONE, q1 = ZERO; + let n = f.n, d = f.d; + while (true) { + const a = n / d; + const q2 = q0 + a * q1; + if (q2 > MAX_DENOMINATOR) { + break; + } + const p2 = p0 + a * p1; + p0 = p1; + q0 = q1; + p1 = p2; + q1 = q2; + const r = n - a * d; + n = d; + d = r; + } + const k = (MAX_DENOMINATOR - q0) / q1; + const p = p0 + k * p1; + const q = q0 + k * q1; + // |p1/q1 - x| <= |p/q - x|, cross-multiplied + const closer = abs(p1 * f.d - f.n * q1) * q <= abs(p * f.d - f.n * q) * q1; + return closer ? new math.Fraction(f.s * p1, q1) : new math.Fraction(f.s * p, q); +} + +// JS number of a Fraction; fraction.js 5 valueOf() is NaN when numerator and +// denominator are both beyond the double range +function fractionToFloat(f) { + const v = f.valueOf(); + if (!isNaN(v)) { + return v; + } + const bits = Math.max(f.n.toString(2).length, f.d.toString(2).length) - 1000; + const shift = BigInt(bits > 0 ? bits : 0); + return Number(f.s * (f.n >> shift)) / Number(f.d >> shift); +} + export class ExpressionEngine { @@ -53,19 +110,19 @@ export class ExpressionEngine { } static add(a, b) { - return math.add(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); + return limitDenominator(math.add(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static subtract(a, b) { - return math.subtract(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); + return limitDenominator(math.subtract(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static divide(a, b) { - return math.divide(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); + return limitDenominator(math.divide(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static multiply(a, b) { - return math.multiply(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); + return limitDenominator(math.multiply(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static round(a, places) { @@ -73,6 +130,10 @@ export class ExpressionEngine { } static toNumber(a) { + if (math.isUnit(a)) { + // Math.js 14+ converts the value of a Unit; a payoff or probability is a number + throw new TypeError('Unexpected type of argument in function fraction (actual: Unit)'); + } let parsed = parseFloat(a); if (parsed === Infinity || parsed === -Infinity) { return parsed; @@ -192,6 +253,9 @@ export class ExpressionEngine { } static toFloat(number) { + if (math.isFraction(number)) { + return fractionToFloat(number); + } return math.number(number); } diff --git a/test/contract/exports.test.js b/test/contract/exports.test.js index 1b803d2..bd58188 100644 --- a/test/contract/exports.test.js +++ b/test/contract/exports.test.js @@ -29,6 +29,12 @@ test('exports: ExpressionEngine only, with the __esModule flag', () => { assert.ok(new ExpressionEngine() instanceof ExpressionEngine); }); +test('the tested artifact contains the expected Math.js version', () => { + // package.json for the sources and fresh builds, the release's version for + // SD_EXPRESSION_ENGINE_BASELINE (the committed dist/ is the 0.1.17 build) + assert.equal(ExpressionEngine.eval('version', false), loaded.expectedMathjs); +}); + test('static and instance API', () => { const statics = Object.getOwnPropertyNames(ExpressionEngine).filter(n => !['length', 'name', 'prototype'].includes(n)); assert.deepEqual(statics.sort(), STATIC); @@ -59,7 +65,7 @@ test('package.json keeps the entry points, the runtime dependencies and the brow }]] }); assert.deepEqual(Object.keys(pkg.dependencies).sort(), ['mathjs', 'sd-random', 'sd-utils']); - assert.equal(pkg.dependencies.mathjs, '9.5.0', 'Math.js is pinned exactly'); + assert.match(pkg.dependencies.mathjs, /^\d+\.\d+\.\d+$/, 'Math.js is pinned exactly'); // sources consumers bundle (index.js -> src/) and the standalone entry for (const file of ['index.js', 'standalone.index.js', 'src/index.js', 'src/expression-engine.js', 'src/mathjs.js']) { assert.ok(fs.existsSync(path.join(loaded.root, file)), file); @@ -157,7 +163,6 @@ test('module bundle: global require exposing sd-expression-engine, cached; sd-ut const code = fs.readFileSync(bundleFile(), 'utf8'); assert.doesNotMatch(code, /Trapezoidal\(0\.0,1\.0,0\.25,0\.75\)/, 'sd-random not bundled'); assert.doesNotMatch(code, /lodash\.com|__lodash_hash_undefined__/, 'sd-utils / lodash not bundled'); - assert.equal(ExpressionEngine.eval('version', false), '9.5.0', 'Math.js 9.5.0 inside the bundle'); }); // Real browserify `require` bundle loaded after the engine, exposing a module diff --git a/test/contract/load.js b/test/contract/load.js index eb0e7e3..3563d00 100644 --- a/test/contract/load.js +++ b/test/contract/load.js @@ -15,6 +15,11 @@ // a CommonJS file exporting the module namespace // SD_EXPRESSION_ENGINE_DIST points at another dist/ directory (default: ./dist), // e.g. the dist/ of the published package extracted from `npm pack`. +// SD_EXPRESSION_ENGINE_BASELINE= tests built artifacts of a release +// against the contract of that release (its Math.js version and the +// version-dependent expectations), e.g. 0.1.17 for the committed dist/ and the +// published package; without it the expectations of the current sources and +// package.json (their Math.js version) apply. // SD_UTILS_BUNDLE / SD_RANDOM_BUNDLE are the `require` bundles loaded before the // module bundle (default: dist/sd-utils.js / dist/sd-random.js of the installed // packages). @@ -35,6 +40,17 @@ const childProcess = require('child_process'); const root = path.resolve(__dirname, '..', '..'); const target = process.env.SD_EXPRESSION_ENGINE_TARGET || 'standalone'; const dist = path.resolve(root, process.env.SD_EXPRESSION_ENGINE_DIST || 'dist'); +// Math.js version of each release whose artifacts can be tested +const BASELINES = {'0.1.17': {mathjs: '9.5.0'}}; +const baseline = process.env.SD_EXPRESSION_ENGINE_BASELINE || null; +if (baseline && !BASELINES[baseline]) { + throw new Error('unknown SD_EXPRESSION_ENGINE_BASELINE ' + baseline + ', known: ' + Object.keys(BASELINES).join(', ')); +} +if (baseline && target === 'source') { + throw new Error('SD_EXPRESSION_ENGINE_BASELINE applies to built artifacts, not to the sources'); +} +const expectedMathjs = baseline ? BASELINES[baseline].mathjs + : JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).dependencies.mathjs; const rng = (function () { const original = Math.random; @@ -169,6 +185,11 @@ function loadSdUtils() { const loaded = load(); loaded.target = target; +loaded.baseline = baseline; +// the Math.js version the tested artifact must contain, and its major version +// for the version-dependent expectations +loaded.expectedMathjs = expectedMathjs; +loaded.mathjsMajor = parseInt(expectedMathjs, 10); loaded.ExpressionEngine = loaded.engine.ExpressionEngine; loaded.isModuleBundle = /^module(\.min)?$/.test(target); loaded.isStandalone = /^standalone(\.min)?$/.test(target); diff --git a/test/contract/mathjs-version.test.js b/test/contract/mathjs-version.test.js new file mode 100644 index 0000000..c983038 --- /dev/null +++ b/test/contract/mathjs-version.test.js @@ -0,0 +1,132 @@ +// Contract points that depend on the Math.js version (0.1.17 / Math.js 9.5.0 and +// the current Math.js of package.json): bounded Fractions in the arithmetic +// helpers, numbers of large Fractions, Units and the JSON form of 0.1.17, and +// the documented behaviour changes of Math.js 10-15 for user expressions. +// Version differences are asserted explicitly for both versions (no skip); +// mathjsMajor is the major version the tested artifact must contain (load.js). +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const {ExpressionEngine, mathjsMajor} = require('./load'); +const {assertFraction} = require('./helpers'); + +const N = ExpressionEngine.toNumber; +const legacy = mathjsMajor < 14; // Math.js 9.5.0 of 0.1.17 + +// exact rational arithmetic with bigint, independent of Math.js / fraction.js +function rational(f) { + const m = /^(-?)(\d+)(?:\/(\d+))?$/.exec(f.toFraction(false)); + return {n: BigInt(m[1] + m[2]), d: BigInt(m[3] || 1)}; +} +function addRational(a, b) { + return {n: a.n * b.d + b.n * a.d, d: a.d * b.d}; +} +function rationalToFloat(r) { + // enough digits for a double: scale to 30 significant digits + const scale = BigInt(10) ** BigInt(30); + return Number((r.n * scale) / r.d) / 1e30; +} + +test('arithmetic helpers keep Fractions bounded: the mean of many evaluated payoffs (probabilistic sensitivity analysis)', () => { + // sd-computations 0.6.1 prob-calculate-step: expectedValue = add(expectedValue, divide(payoff, numberOfRuns)) + let seed = 42; + const draw = () => { + seed = (seed * 1103515245 + 12345) % 2147483648; + return seed / 2147483648; + }; + const runs = 500; + let mean = N(0); + let exact = {n: BigInt(0), d: BigInt(1)}; + for (let i = 0; i < runs; i++) { + const payoff = ExpressionEngine.eval('2500 * pr - 250 * (1 - pr)', true, {pr: 0.1 + 0.8 * draw()}); + const share = ExpressionEngine.divide(payoff, runs); + mean = ExpressionEngine.add(mean, share); + const p = rational(payoff); + exact = addRational(exact, {n: p.n, d: p.d * BigInt(runs)}); + } + // Math.js 9.5.0 keeps numerator and denominator as JS numbers; the current + // engine rounds results with a denominator above 2^53 - 1 to the closest + // fraction with a smaller denominator. Either way 16 digits at most. + assert.ok(String(mean.d).length <= 16, 'denominator ' + String(mean.d)); + const value = ExpressionEngine.toFloat(mean); + const reference = rationalToFloat(exact); + assert.ok(Number.isFinite(value), String(value)); + // each of the 2 * 500 rounded operations is off by at most about 2^-52 of the + // value, which stays below 500 * 2.2e-16 * 2 = 2.2e-13 relative; 1e-12 leaves + // room for the conversions of the payoffs + assert.ok(Math.abs(value - reference) / Math.abs(reference) < 1e-12, value + ' vs ' + reference); +}); + +test('arithmetic helpers: small results stay exact', () => { + assertFraction(ExpressionEngine.add('1/3', '1/6'), '1/2'); + assertFraction(ExpressionEngine.divide(ExpressionEngine.subtract(1, '1/7'), 3), '2/7'); + assertFraction(ExpressionEngine.multiply('2/3', '3/4'), '1/2'); + // denominator 2^53 - 1 is kept as it is + const limit = String(Number.MAX_SAFE_INTEGER); + assertFraction(ExpressionEngine.add('1/' + limit, 0), '1/' + limit); +}); + +test('toFloat of a Fraction beyond the double range of numerator and denominator', {skip: legacy && 'Math.js 9.5.0: fraction.js 4 holds no bigint Fractions'}, () => { + // built through the reviver (toNumber of such a string would stop at its parseFloat Infinity check) + const reviver = new ExpressionEngine().getJsonReviver(); + const fraction = (n, d) => JSON.parse(JSON.stringify({mathjs: 'Fraction', n, d}), reviver); + const zeros = '0'.repeat(400); + assert.equal(ExpressionEngine.toFloat(fraction('1' + zeros, '3' + zeros)), 1 / 3); + assert.equal(ExpressionEngine.toFloat(fraction('-2' + zeros, '3' + zeros)), -2 / 3); + assert.equal(ExpressionEngine.toFloat(fraction('1' + zeros, '3')), Infinity); +}); + +test('the JSON form written by 0.1.17 (numbers) is revived exactly', () => { + const reviver = new ExpressionEngine().getJsonReviver(); + const back = JSON.parse('{"a": {"mathjs": "Fraction", "n": -1, "d": 3}, "b": {"mathjs": "Fraction", "n": 878291371642703, "d": 3279115770832143}}', reviver); + assertFraction(back.a, '-1/3'); + assertFraction(back.b, '878291371642703/3279115770832143'); +}); + +test('eval with asNumber: a Unit result is rejected', () => { + for (const expr of ['2 cm + 3 mm', '5 inch to cm', 'unit(5, "m")']) { + assert.throws(() => ExpressionEngine.eval(expr), TypeError, expr); + assert.equal(ExpressionEngine.eval(expr, false).isUnit, true, expr); + } +}); + +// Behaviour changes of Math.js for user expressions (Math.js HISTORY.md), asserted +// for both versions. Changing the meaning of accepted expressions is a decision of +// the product owner; the upgrade PR lists them. +test('[version difference] implicit multiplication after a division with a negative constant numerator (Math.js 11)', () => { + assertFraction(ExpressionEngine.eval('-1 / 2 x', true, {x: 2}), legacy ? '-1/4' : '-1'); + // unchanged + assertFraction(ExpressionEngine.eval('1 / 2 x', true, {x: 2}), '1'); + assertFraction(ExpressionEngine.eval('-1 / (2 x)', true, {x: 2}), '-1/4'); +}); + +test('[version difference] element-wise sqrt, exp, sin of matrices (removed in Math.js 11)', () => { + if (legacy) { + assert.deepEqual(ExpressionEngine.eval('sqrt([4, 9])', false).toArray(), [2, 3]); + } else { + assert.throws(() => ExpressionEngine.eval('sqrt([4, 9])', false)); + } + assert.deepEqual(ExpressionEngine.eval('map([4, 9], sqrt)', false).toArray(), [2, 3]); +}); + +test('[version difference] mod with a negative divisor, empty true branch of ?:, size(), new operators', () => { + if (legacy) { + assert.throws(() => ExpressionEngine.eval('10 mod -3', false), /negative divisor/); + assert.equal(ExpressionEngine.eval('1 ? : 2', false), 1); + assert.equal(ExpressionEngine.eval('size([1, 2, 3])', false).isMatrix, true); + assert.equal(ExpressionEngine.validate('a ?? 1'), false); + } else { + assert.equal(ExpressionEngine.eval('10 mod -3', false), -2); + assert.throws(() => ExpressionEngine.eval('1 ? : 2', false), SyntaxError); + assert.deepEqual(ExpressionEngine.eval('size([1, 2, 3])', false), [3]); + assert.equal(ExpressionEngine.eval('a ?? 1', false, {a: null}), 1); + } +}); + +test('[version difference] integers beyond 2^53 in numeric strings', () => { + // toNumber reads the string itself: fraction.js 5 (bigint) keeps every digit + assertFraction(ExpressionEngine.eval('9007199254740993'), legacy ? '9007199254740992' : '9007199254740993'); + // expressions are still evaluated with JS numbers + assert.equal(ExpressionEngine.eval('2^53 + 1', false), 9007199254740992); +}); diff --git a/test/contract/numbers.test.js b/test/contract/numbers.test.js index 2be4d9a..7ea2e7e 100644 --- a/test/contract/numbers.test.js +++ b/test/contract/numbers.test.js @@ -6,7 +6,7 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const {ExpressionEngine} = require('./load'); +const {ExpressionEngine, mathjsMajor} = require('./load'); const {assertFraction, knownBug} = require('./helpers'); const N = ExpressionEngine.toNumber; @@ -75,8 +75,11 @@ test('add, subtract, multiply, divide: exact on Fractions, arguments converted w test('arithmetic with Infinity [current behaviour]: only ±Infinity together are numbers, Infinity with a Fraction throws', () => { assert.ok(Number.isNaN(ExpressionEngine.add(Infinity, -Infinity))); assert.equal(ExpressionEngine.add(Infinity, Infinity), Infinity); - assert.throws(() => ExpressionEngine.add(Infinity, 1), /Cannot implicitly convert a number to a Fraction/); - assert.throws(() => ExpressionEngine.divide(1, Infinity), /Cannot implicitly convert a number to a Fraction/); + // the message is Math.js': 9.x refuses the implicit conversion, 14+ (fraction.js 5, + // bigint) cannot convert Infinity to a bigint + const message = mathjsMajor < 14 ? /Cannot implicitly convert a number to a Fraction/ : /Infinity cannot be converted to a BigInt/; + assert.throws(() => ExpressionEngine.add(Infinity, 1), message); + assert.throws(() => ExpressionEngine.divide(1, Infinity), message); }); test('compare: -1/0/1 for Fractions, ±Infinity handled before Math.js', () => { diff --git a/test/contract/run.js b/test/contract/run.js index 657bf5e..234332d 100644 --- a/test/contract/run.js +++ b/test/contract/run.js @@ -5,7 +5,10 @@ // node test/contract/run.js --targets=module,source // node test/contract/run.js --dist= the bundles of another dist/ directory // node test/contract/run.js --published[=0.1.17] the bundles of the package on the npm registry -// (npm pack into a temporary directory) +// (npm pack into a temporary directory), tested +// against the contract of that release +// node test/contract/run.js --baseline[=0.1.17] the committed dist/ (the build of that release) +// against the contract of that release // // Each target runs in its own `node --test` process (SD_EXPRESSION_ENGINE_TARGET, // SD_EXPRESSION_ENGINE_DIST, see load.js). Exits with 1 if any target fails. @@ -27,6 +30,10 @@ const BUNDLES = ['standalone', 'standalone.min', 'module', 'module.min']; let dist = option('dist'); let tmp = null; const published = option('published'); +const baselineOption = option('baseline'); +// the committed dist/ is the 0.1.17 release build until the next release +const BASELINE = '0.1.17'; +let baseline = baselineOption ? (baselineOption === true ? BASELINE : baselineOption) : null; function npm(npmArgs, cwd) { // npm is npm.cmd on Windows; run it through the shell there @@ -38,7 +45,8 @@ function npm(npmArgs, cwd) { } if (published) { - const version = published === true ? require(path.join(root, 'package.json')).version : published; + const version = published === true ? BASELINE : published; + baseline = version; tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sd-ee-published-')); const tgz = npm(['pack', 'sd-expression-engine@' + version, '--silent'], tmp).trim().split(/\r?\n/).pop(); childProcess.execFileSync('tar', ['-xzf', tgz], {cwd: tmp}); @@ -47,14 +55,17 @@ if (published) { } const targetsOption = option('targets'); -const targets = targetsOption ? String(targetsOption).split(',') : (dist ? BUNDLES : BUNDLES.concat('source')); +const targets = targetsOption ? String(targetsOption).split(',') : (dist || baseline ? BUNDLES : BUNDLES.concat('source')); const files = fs.readdirSync(__dirname).filter(f => f.endsWith('.test.js')).sort().map(f => path.join(__dirname, f)); const failed = []; try { for (const target of targets) { - console.log('\n=== contract tests: ' + target + (dist ? ' (' + dist + ')' : '')); + console.log('\n=== contract tests: ' + target + (dist ? ' (' + dist + ')' : '') + (baseline ? ', contract of ' + baseline : '')); const env = Object.assign({}, process.env, {SD_EXPRESSION_ENGINE_TARGET: target}); + if (baseline) { + env.SD_EXPRESSION_ENGINE_BASELINE = baseline; + } if (dist) { env.SD_EXPRESSION_ENGINE_DIST = path.resolve(dist); } diff --git a/test/contract/serialization.test.js b/test/contract/serialization.test.js index 9cec8c5..a8e07f9 100644 --- a/test/contract/serialization.test.js +++ b/test/contract/serialization.test.js @@ -13,7 +13,7 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const {ExpressionEngine, sdUtils} = require('./load'); +const {ExpressionEngine, sdUtils, mathjsMajor} = require('./load'); const {assertFraction, readFixture, FIXTURES, evaluateDiagram} = require('./helpers'); const {Utils} = sdUtils; @@ -80,7 +80,8 @@ test('isExpressionObject: objects with a mathjs field (the JSON form of Math.js } // a live Fraction has no own mathjs field: the replacer sees its toJSON() form assert.ok(!ExpressionEngine.isExpressionObject(N('1/3'))); - assert.deepEqual(N('-1/3').toJSON(), {mathjs: 'Fraction', n: -1, d: 3}); + // [version difference] Math.js 14+ (fraction.js 5, bigint) writes n and d as strings + assert.deepEqual(N('-1/3').toJSON(), mathjsMajor < 14 ? {mathjs: 'Fraction', n: -1, d: 3} : {mathjs: 'Fraction', n: '-1', d: '3'}); }); test('getJsonReplacer in JSON.stringify: Fractions as fraction strings, other values unchanged', () => { From e1171b6075f489c541563a59e83d3a4be98c1eee Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 12:08:07 +0000 Subject: [PATCH 2/6] [deps] keep small values and relative precision in the Fraction rounding The denominator bound of the arithmetic helpers was absolute: results below about 1e-16 became 0 (divide(1, 1e20), multiply(divide(1, 1e20), 1e20)), and toFloat lost Fractions beyond the double range (Infinity for (10^310 + 1) / 1009, 0 for (2^1200 + 1) / (2^2200 + 1)). Results stay exact while numerator and denominator have at most 64 bits and are otherwise replaced by the first continued-fraction convergent within a relative error of 2^-64; toFloat divides with bigint to 64 significant bits and scales by a power of two. Tests against exact rational arithmetic: the reported cases, random operands from 1e-30 to 1e30, a sum of terms between 1e-25 and 1e25, random large Fractions in toFloat. --- CHANGELOG.md | 18 +-- README.md | 3 +- src/expression-engine.js | 91 ++++++++------ test/contract/mathjs-version.test.js | 181 ++++++++++++++++++++++----- 4 files changed, 221 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aae475..1b90afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,18 @@ ### Dependencies: Math.js 15.2.0 (from 9.5.0) - **Runtime requirements**: Math.js 13+ requires ES2020 and `BigInt` (Chrome and - Edge 80+, Firefox 74+, Safari 14+; no Internet Explorer). The bundles contain + Edge 80+, Firefox 74+, Safari 14+ from the ES2020 features used, not tested in + those browsers; no Internet Explorer). The bundles contain Math.js as published (ES2020 syntax); the package sources still compile to ES5. -- Fractions use fraction.js 5 (`bigint`). The arithmetic helpers (`add`, - `subtract`, `multiply`, `divide`) round a result whose denominator exceeds - 2^53 - 1 to the closest fraction with a smaller denominator, so long sums (the - mean of a probabilistic sensitivity analysis) stay fast and finite as with - Math.js 9 (without it they grew to thousands of digits and `toFloat` returned - `NaN`); `toFloat` handles Fractions beyond the double range. +- Fractions use fraction.js 5 (`bigint`) and are exact up to any size; long + sums (the mean of a probabilistic sensitivity analysis) grew to thousands of + digits, got slow and `toFloat` returned `NaN`. The arithmetic helpers (`add`, + `subtract`, `multiply`, `divide`) therefore keep a result exact while its + numerator and denominator have at most 64 bits and otherwise replace it with + a simpler fraction (continued-fraction convergent) within a relative error of 2^-64 (0.1.17: doubles, + exact up to 53 bits, relative error up to 2^-52 per operation). Small values + keep their magnitude. `toFloat` is accurate for Fractions beyond the double + range. - `eval(expr, true)` rejects a `Unit` result as before (Math.js 14+ would convert the value of the unit). - The JSON form of a Fraction (`toJSON`, data model DTO of the job repository) diff --git a/README.md b/README.md index 0784edc..c000027 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ Built files (`dist/`, committed, rebuilt by the release): The plain files carry an inline source map, the minified ones a `.map` file. Runtime requirements: Math.js 13+ requires ES2020 and `BigInt` (Chrome and Edge 80+, -Firefox 74+, Safari 14+, no Internet Explorer). The package sources compile to +Firefox 74+, Safari 14+ from the ES2020 features used, not tested in those +browsers; no Internet Explorer). The package sources compile to ES5; the bundles and consumer bundles contain Math.js as published (ES2020). Releases up to 0.1.17 (Math.js 9.5.0) ran on ES5 engines. diff --git a/src/expression-engine.js b/src/expression-engine.js index ad8db51..4b5e89f 100644 --- a/src/expression-engine.js +++ b/src/expression-engine.js @@ -15,12 +15,18 @@ sdRandom.functionNameList.forEach(fnName => { // Math.js 14+ (fraction.js 5) computes Fractions exactly with bigint numerators // and denominators: a sum of many evaluated (float) values, e.g. the expected -// values of the probabilistic sensitivity analysis, grows without limit, gets -// slow and its JS number (valueOf) becomes NaN. With Math.js 9 (fraction.js 4, -// JS numbers) Fractions stayed within the safe integer range. The arithmetic -// helpers therefore round a result whose denominator exceeds MAX_DENOMINATOR to -// the closest fraction with a denominator up to MAX_DENOMINATOR. -const MAX_DENOMINATOR = BigInt(Number.MAX_SAFE_INTEGER); +// values of the probabilistic sensitivity analysis, grows to thousands of digits, +// gets slow and its JS number (valueOf) becomes NaN. With Math.js 9 (fraction.js +// 4) numerator and denominator were doubles: about 16 significant digits at any +// magnitude. The arithmetic helpers therefore keep a result exact while its +// numerator and denominator have at most MAX_BITS bits, and otherwise replace it +// with the first continued-fraction convergent within a relative error of +// 2^-RELATIVE_BITS (about 5e-20). Small values keep their magnitude; results +// are exact up to 64-bit numerators and denominators (Math.js 9: 53 bits) and +// otherwise about 2000 times more precise than a double. The bounds keep the +// cost of long sums close to Math.js 9. +const MAX_BITS = 64; +const RELATIVE_BITS = BigInt(64); const ZERO = BigInt(0); const ONE = BigInt(1); @@ -28,20 +34,22 @@ function abs(v) { return v < ZERO ? -v : v; } -// best rational approximation with a bounded denominator (continued fractions) -function limitDenominator(f) { - if (!math.isFraction(f) || f.d <= MAX_DENOMINATOR) { +function bitLength(v) { + return v === ZERO ? 0 : abs(v).toString(2).length; +} + +function roundFraction(f) { + if (!math.isFraction(f) || (bitLength(f.n) <= MAX_BITS && bitLength(f.d) <= MAX_BITS)) { return f; } + // f.n > 0 here (zero is 0/1); convergents p1/q1 of f.n/f.d + const x = f.n, y = f.d; let p0 = ZERO, q0 = ONE, p1 = ONE, q1 = ZERO; - let n = f.n, d = f.d; - while (true) { + let n = x, d = y; + while (d !== ZERO) { const a = n / d; - const q2 = q0 + a * q1; - if (q2 > MAX_DENOMINATOR) { - break; - } - const p2 = p0 + a * p1; + const p2 = a * p1 + p0; + const q2 = a * q1 + q0; p0 = p1; q0 = q1; p1 = p2; @@ -49,25 +57,38 @@ function limitDenominator(f) { const r = n - a * d; n = d; d = r; + // |p1/q1 - x/y| <= 2^-RELATIVE_BITS * x/y, cross-multiplied + if ((abs(p1 * y - x * q1) << RELATIVE_BITS) <= x * q1) { + break; + } } - const k = (MAX_DENOMINATOR - q0) / q1; - const p = p0 + k * p1; - const q = q0 + k * q1; - // |p1/q1 - x| <= |p/q - x|, cross-multiplied - const closer = abs(p1 * f.d - f.n * q1) * q <= abs(p * f.d - f.n * q) * q1; - return closer ? new math.Fraction(f.s * p1, q1) : new math.Fraction(f.s * p, q); + return new math.Fraction(f.s * p1, q1); } -// JS number of a Fraction; fraction.js 5 valueOf() is NaN when numerator and -// denominator are both beyond the double range +// JS number of a Fraction. fraction.js 5 valueOf() is Number(n) / Number(d), +// which is Infinity or NaN when n or d is beyond the double range (about 2^1024) +// although the quotient is not; such Fractions are divided with bigint to 64 +// significant bits and scaled by a power of two. function fractionToFloat(f) { - const v = f.valueOf(); - if (!isNaN(v)) { - return v; - } - const bits = Math.max(f.n.toString(2).length, f.d.toString(2).length) - 1000; - const shift = BigInt(bits > 0 ? bits : 0); - return Number(f.s * (f.n >> shift)) / Number(f.d >> shift); + const nBits = bitLength(f.n); + const dBits = bitLength(f.d); + if (nBits <= 1000 && dBits <= 1000) { + return f.valueOf(); + } + if (f.n === ZERO) { + return 0; + } + // q = floor(n * 2^-e / d) has 64 or 65 bits + const e = nBits - dBits - 64; + const q = e >= 0 ? f.n / (f.d << BigInt(e)) : (f.n << BigInt(-e)) / f.d; + let v = Number(q); + let k = e; + while (k !== 0) { + const step = Math.max(-1000, Math.min(1000, k)); + v *= Math.pow(2, step); + k -= step; + } + return f.s < ZERO ? -v : v; } @@ -110,19 +131,19 @@ export class ExpressionEngine { } static add(a, b) { - return limitDenominator(math.add(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return roundFraction(math.add(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static subtract(a, b) { - return limitDenominator(math.subtract(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return roundFraction(math.subtract(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static divide(a, b) { - return limitDenominator(math.divide(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return roundFraction(math.divide(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static multiply(a, b) { - return limitDenominator(math.multiply(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return roundFraction(math.multiply(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); } static round(a, places) { diff --git a/test/contract/mathjs-version.test.js b/test/contract/mathjs-version.test.js index c983038..48b3040 100644 --- a/test/contract/mathjs-version.test.js +++ b/test/contract/mathjs-version.test.js @@ -1,6 +1,6 @@ // Contract points that depend on the Math.js version (0.1.17 / Math.js 9.5.0 and -// the current Math.js of package.json): bounded Fractions in the arithmetic -// helpers, numbers of large Fractions, Units and the JSON form of 0.1.17, and +// the current Math.js of package.json): precision of the arithmetic helpers +// against exact rational arithmetic, numbers of large Fractions, Units and the JSON form of 0.1.17, and // the documented behaviour changes of Math.js 10-15 for user expressions. // Version differences are asserted explicitly for both versions (no skip); // mathjsMajor is the major version the tested artifact must contain (load.js). @@ -15,20 +15,64 @@ const N = ExpressionEngine.toNumber; const legacy = mathjsMajor < 14; // Math.js 9.5.0 of 0.1.17 // exact rational arithmetic with bigint, independent of Math.js / fraction.js +const B = v => BigInt(v); +const babs = v => (v < B(0) ? -v : v); function rational(f) { const m = /^(-?)(\d+)(?:\/(\d+))?$/.exec(f.toFraction(false)); - return {n: BigInt(m[1] + m[2]), d: BigInt(m[3] || 1)}; + return {n: B(m[1] + m[2]), d: B(m[3] || 1)}; } function addRational(a, b) { return {n: a.n * b.d + b.n * a.d, d: a.d * b.d}; } +// correctly rounded double of n/d: 25 significant decimal digits, parsed by Number() function rationalToFloat(r) { - // enough digits for a double: scale to 30 significant digits - const scale = BigInt(10) ** BigInt(30); - return Number((r.n * scale) / r.d) / 1e30; + if (r.n === B(0)) { + return 0; + } + const sign = (r.n < B(0)) !== (r.d < B(0)) ? '-' : ''; + const n = babs(r.n), d = babs(r.d); + let k = 25 - (n.toString().length - d.toString().length); + const q = k >= 0 ? (n * B(10) ** B(k)) / d : n / (d * B(10) ** B(-k)); + return Number(sign + q.toString() + 'e' + (-k)); +} +function relativeError(value, reference) { + return reference === 0 ? Math.abs(value) : Math.abs(value - reference) / Math.abs(reference); +} +// deterministic generator of bigints with up to `bits` bits +function generator(seed) { + let s = seed >>> 0; + const next = () => { + s = (s + 0x6D2B79F5) >>> 0; + let t = Math.imul(s ^ (s >>> 15), s | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0); + }; + return { + int: (max) => next() % max, + big: (bits) => { + let v = B(0); + for (let i = 0; i < bits; i += 32) { + v = (v << B(32)) | B(next()); + } + return (v >> B(Math.max(0, Math.ceil(bits / 32) * 32 - bits))) | B(1); + } + }; } +const reviver = new ExpressionEngine().getJsonReviver(); +const fraction = (n, d) => JSON.parse(JSON.stringify({mathjs: 'Fraction', n: String(n), d: String(d)}), reviver); -test('arithmetic helpers keep Fractions bounded: the mean of many evaluated payoffs (probabilistic sensitivity analysis)', () => { +test('arithmetic helpers keep small values (regressions of a denominator bound)', () => { + // 0.1.17 gives these exactly; an absolute bound of the denominator turned them into 0 + assert.equal(ExpressionEngine.toFloat(ExpressionEngine.divide(1, 1e20)), 1e-20); + assert.equal(ExpressionEngine.toFloat(ExpressionEngine.add('1/100000000000000000000', 0)), 1e-20); + assertFraction(ExpressionEngine.multiply(ExpressionEngine.divide(1, 1e20), 1e20), '1'); + assertFraction(ExpressionEngine.divide(1, 1e20), '1/100000000000000000000'); + // [version difference] fraction.js 4 stores 10^20 + 1 as the double 10^20, so 0.1.17 gives 0 + assertFraction(ExpressionEngine.subtract('1/100000000000000000000', '1/100000000000000000001'), + legacy ? '0' : '1/10000000000000000000100000000000000000000'); +}); + +test('arithmetic helpers: the mean of many evaluated payoffs (probabilistic sensitivity analysis) stays bounded and accurate', () => { // sd-computations 0.6.1 prob-calculate-step: expectedValue = add(expectedValue, divide(payoff, numberOfRuns)) let seed = 42; const draw = () => { @@ -37,44 +81,123 @@ test('arithmetic helpers keep Fractions bounded: the mean of many evaluated payo }; const runs = 500; let mean = N(0); - let exact = {n: BigInt(0), d: BigInt(1)}; + let exact = {n: B(0), d: B(1)}; for (let i = 0; i < runs; i++) { const payoff = ExpressionEngine.eval('2500 * pr - 250 * (1 - pr)', true, {pr: 0.1 + 0.8 * draw()}); - const share = ExpressionEngine.divide(payoff, runs); - mean = ExpressionEngine.add(mean, share); + mean = ExpressionEngine.add(mean, ExpressionEngine.divide(payoff, runs)); const p = rational(payoff); - exact = addRational(exact, {n: p.n, d: p.d * BigInt(runs)}); + exact = addRational(exact, {n: p.n, d: p.d * B(runs)}); } - // Math.js 9.5.0 keeps numerator and denominator as JS numbers; the current - // engine rounds results with a denominator above 2^53 - 1 to the closest - // fraction with a smaller denominator. Either way 16 digits at most. - assert.ok(String(mean.d).length <= 16, 'denominator ' + String(mean.d)); const value = ExpressionEngine.toFloat(mean); const reference = rationalToFloat(exact); - assert.ok(Number.isFinite(value), String(value)); - // each of the 2 * 500 rounded operations is off by at most about 2^-52 of the - // value, which stays below 500 * 2.2e-16 * 2 = 2.2e-13 relative; 1e-12 leaves - // room for the conversions of the payoffs - assert.ok(Math.abs(value - reference) / Math.abs(reference) < 1e-12, value + ' vs ' + reference); + if (legacy) { + // fraction.js 4: doubles, each of the 1000 operations off by up to 2^-52 + assert.ok(String(mean.d).length <= 16, 'denominator ' + String(mean.d)); + assert.ok(relativeError(value, reference) < 1e-12, value + ' vs ' + reference); + } else { + // exact while numerator and denominator have <= 64 bits, otherwise rounded with a + // relative error below 2^-64: 1000 * 5.4e-20 = 5.4e-17, plus 1 ulp (2.2e-16) of toFloat + assert.ok(String(mean.d).length <= 20, 'denominator ' + String(mean.d)); + assert.ok(relativeError(value, reference) <= 4.5e-16, value + ' vs ' + reference); + } +}); + +test('arithmetic helpers: random operands of 1e-30 to 1e30 against exact rational arithmetic', {skip: legacy && 'Math.js 9.5.0: the operands (up to 160-bit integers) are not representable in fraction.js 4'}, () => { + const g = generator(7); + const ops = { + add: (a, b) => ({n: a.n * b.d + b.n * a.d, d: a.d * b.d}), + subtract: (a, b) => ({n: a.n * b.d - b.n * a.d, d: a.d * b.d}), + multiply: (a, b) => ({n: a.n * b.n, d: a.d * b.d}), + divide: (a, b) => ({n: a.n * b.d, d: a.d * b.n}) + }; + const operand = () => { + // p/q * 10^k with 1-80 bit p and q + const k = g.int(61) - 30; + let n = g.big(1 + g.int(80)), d = g.big(1 + g.int(80)); + if (k >= 0) { + n *= B(10) ** B(k); + } else { + d *= B(10) ** B(-k); + } + if (g.int(2)) { + n = -n; + } + return {n, d}; + }; + let worst = 0; + for (let i = 0; i < 2000; i++) { + const a = operand(), b = operand(); + for (const [name, op] of Object.entries(ops)) { + const r = ExpressionEngine[name](fraction(a.n, a.d), fraction(b.n, b.d)); + const e = op(a, b); + // relative error of the Fraction itself, computed exactly: |r - e| / |e| + const x = rational(r); + const errN = babs(x.n * e.d - e.n * x.d); + const errD = babs(x.d * e.n); + if (errN !== B(0)) { + // <= 2^-64 (with the sign of e.n accounted for by babs) + assert.ok((errN << B(64)) <= errD, name + ' relative error above 2^-64'); + worst = Math.max(worst, rationalToFloat({n: errN, d: errD})); + } + assert.notEqual(x.n === B(0), e.n !== B(0) ? true : false, name + ' kept a non-zero result non-zero'); + } + } + assert.ok(worst <= Math.pow(2, -64)); +}); + +test('arithmetic helpers: a positive sum of 300 terms between 1e-25 and 1e25', {skip: legacy && 'Math.js 9.5.0: see the previous test'}, () => { + const g = generator(11); + let sum = N(0); + let exact = {n: B(0), d: B(1)}; + for (let i = 0; i < 300; i++) { + const k = g.int(51) - 25; + let n = g.big(30), d = g.big(30); + if (k >= 0) { + n *= B(10) ** B(k); + } else { + d *= B(10) ** B(-k); + } + sum = ExpressionEngine.add(sum, fraction(n, d)); + exact = addRational(exact, {n, d}); + } + // 300 roundings of at most 2^-64 each (no cancellation), plus 1 ulp of toFloat + assert.ok(relativeError(ExpressionEngine.toFloat(sum), rationalToFloat(exact)) <= 4.5e-16); }); test('arithmetic helpers: small results stay exact', () => { assertFraction(ExpressionEngine.add('1/3', '1/6'), '1/2'); assertFraction(ExpressionEngine.divide(ExpressionEngine.subtract(1, '1/7'), 3), '2/7'); assertFraction(ExpressionEngine.multiply('2/3', '3/4'), '1/2'); - // denominator 2^53 - 1 is kept as it is const limit = String(Number.MAX_SAFE_INTEGER); assertFraction(ExpressionEngine.add('1/' + limit, 0), '1/' + limit); }); -test('toFloat of a Fraction beyond the double range of numerator and denominator', {skip: legacy && 'Math.js 9.5.0: fraction.js 4 holds no bigint Fractions'}, () => { - // built through the reviver (toNumber of such a string would stop at its parseFloat Infinity check) - const reviver = new ExpressionEngine().getJsonReviver(); - const fraction = (n, d) => JSON.parse(JSON.stringify({mathjs: 'Fraction', n, d}), reviver); - const zeros = '0'.repeat(400); - assert.equal(ExpressionEngine.toFloat(fraction('1' + zeros, '3' + zeros)), 1 / 3); - assert.equal(ExpressionEngine.toFloat(fraction('-2' + zeros, '3' + zeros)), -2 / 3); - assert.equal(ExpressionEngine.toFloat(fraction('1' + zeros, '3')), Infinity); +test('toFloat of Fractions beyond the double range of numerator or denominator', {skip: legacy && 'Math.js 9.5.0: fraction.js 4 holds no bigint Fractions'}, () => { + const ten = B(10), two = B(2); + // not reducible: the quotient is in the double range, numerator or denominator is not + const cases = [ + [ten ** B(310) + B(1), B(1009)], // 9.9108e306 + [two ** B(1200) + B(1), two ** B(2200) + B(1)], // 9.3326e-302 + [-(ten ** B(400) + B(7)), B(3) * ten ** B(400) + B(1)], // -1/3 + [ten ** B(400) + B(1), B(3)], // Infinity + [B(1), ten ** B(400) + B(3)], // 0 (below the double range) + [B(7), two ** B(1074) + B(1)] // subnormal + ]; + for (const [n, d] of cases) { + const value = ExpressionEngine.toFloat(fraction(n, d)); + const reference = rationalToFloat({n, d}); + assert.ok(value === reference || relativeError(value, reference) <= Math.pow(2, -52) || + Math.abs(value - reference) <= Number.MIN_VALUE, n.toString().slice(0, 12) + '... / ' + d.toString().slice(0, 12) + '...: ' + value + ' vs ' + reference); + } + // random non-reducing pairs with up to 2000 bits, quotients across the double range + const g = generator(3); + for (let i = 0; i < 500; i++) { + const n = g.big(1 + g.int(2000)), d = g.big(1 + g.int(2000)); + const value = ExpressionEngine.toFloat(fraction(n, d)); + const reference = rationalToFloat({n, d}); + assert.ok(value === reference || relativeError(value, reference) <= Math.pow(2, -52) || + Math.abs(value - reference) <= Number.MIN_VALUE, value + ' vs ' + reference); + } }); test('the JSON form written by 0.1.17 (numbers) is revived exactly', () => { From 0c0bd47683ac927b4721847a858bd57c03527b64 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 13:00:10 +0000 Subject: [PATCH 3/6] [deps] keep Fraction arithmetic exact, round toFloat correctly, list Math.js changes Revert the rounding of add/subtract/multiply/divide: it changed values (x + 0 != x for x = 2^80 / 3) and decisions on ties; the arithmetic is exact again as Math.js 15 computes it. Bounding the long sums of the probabilistic sensitivity analysis belongs to the aggregation in sd-computations. toFloat of a Fraction returns the double nearest to it, ties to even, including subnormal results and overflow to Infinity (bigint division with a sticky bit). Reference cases (midpoints, ties, subnormals, overflow, random large Fractions) generated with Python's fractions.Fraction. Version-difference tests for the reproduced Math.js 10-15 changes of user expressions (percent and mod precedence, implicit multiplication, comparison tolerances, isInteger, mod round-off, matrix functions, print indices, ...); docs/MATHJS_UPGRADE.md lists their sources and proposes the handling of old diagrams for decision. --- CHANGELOG.md | 27 +- docs/MATHJS_UPGRADE.md | 130 +++ src/expression-engine.js | 117 +-- test/contract/fixtures/README.md | 5 + test/contract/fixtures/tofloat_cases.json | 1133 +++++++++++++++++++++ test/contract/fixtures/tofloat_cases.py | 76 ++ test/contract/mathjs-version.test.js | 293 +++--- 7 files changed, 1550 insertions(+), 231 deletions(-) create mode 100644 docs/MATHJS_UPGRADE.md create mode 100644 test/contract/fixtures/tofloat_cases.json create mode 100644 test/contract/fixtures/tofloat_cases.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b90afc..08776f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,27 +8,22 @@ Edge 80+, Firefox 74+, Safari 14+ from the ES2020 features used, not tested in those browsers; no Internet Explorer). The bundles contain Math.js as published (ES2020 syntax); the package sources still compile to ES5. -- Fractions use fraction.js 5 (`bigint`) and are exact up to any size; long - sums (the mean of a probabilistic sensitivity analysis) grew to thousands of - digits, got slow and `toFloat` returned `NaN`. The arithmetic helpers (`add`, - `subtract`, `multiply`, `divide`) therefore keep a result exact while its - numerator and denominator have at most 64 bits and otherwise replace it with - a simpler fraction (continued-fraction convergent) within a relative error of 2^-64 (0.1.17: doubles, - exact up to 53 bits, relative error up to 2^-52 per operation). Small values - keep their magnitude. `toFloat` is accurate for Fractions beyond the double - range. +- Fractions use fraction.js 5 (`bigint`): exact at any size (0.1.17: doubles, + exact up to 2^53). The arithmetic helpers stay exact; long sums such as the + mean of a probabilistic sensitivity analysis grow without limit; the + aggregation in sd-computations has to bound them (separate change there). `toFloat` returns the correctly rounded double (ties to + even) also for Fractions beyond the double range (fraction.js 5 `valueOf()` + gives Infinity or NaN there). - `eval(expr, true)` rejects a `Unit` result as before (Math.js 14+ would convert the value of the unit). - The JSON form of a Fraction (`toJSON`, data model DTO of the job repository) writes `n` and `d` as strings; the 0.1.17 form with numbers is still read. Saved diagrams are unchanged (Fractions are saved as fraction strings). -- Behaviour of Math.js for user expressions (Math.js 10-15 changelogs, checked - against the expressions of saved diagrams, which give the same values): - `-1 / 2 x` is `(-1/2) * x` (was `-1 / (2 x)`); element-wise `sqrt`, `exp`, - `sin` etc. of matrices throw (use `map`); `mod` accepts negative divisors; - `cond ? : b` is an error; `size()` returns an array; `x[i, :]` returns a - vector; integers in numeric strings beyond 2^53 keep all digits; new syntax - `??`, `?.` and new functions (`isFinite`, `num`, `den`, `bernoulli`, ...). +- Behaviour of Math.js for user expressions changes in many places, some of them + silently: precedence of `%` (percent and modulus) and `mod`, implicit + multiplication after a signed constant division, tolerances of comparisons and + `isInteger`, `mod` round-off, matrix functions, `print` indices, `typeOf`, … + (`docs/MATHJS_UPGRADE.md`, with a proposal for old diagrams). - Security: Math.js 15.2.0 fixes GHSA-jvff-x2qm-6286, GHSA-29qv-4j9f-fjw5 (CVE-2026-40897) and GHSA-5v89-rwgr-qj6g (CVE-2026-41139), which affect 13.1.0 - 15.1.x (not 9.5.0). diff --git a/docs/MATHJS_UPGRADE.md b/docs/MATHJS_UPGRADE.md new file mode 100644 index 0000000..e5d96ed --- /dev/null +++ b/docs/MATHJS_UPGRADE.md @@ -0,0 +1,130 @@ +# Math.js 9.5.0 → 15.2.0: behaviour changes and old diagrams + +Status: proposal for decision. Nothing of the handling of old diagrams below +is implemented; the engine (PR "upgrade Math.js to 15.2.0") only makes the +differences explicit in `test/contract/mathjs-version.test.js` (`DIFFERENCES`, +the complete list of reproduced cases, asserted for both versions). + +## Where the differences come from + +1. **Math.js itself** (HISTORY.md 10.0 - 15.2, each case reproduced): + - parser: `%` before a term is modulus and unary percent binds tighter + (15.0: `100 + 3% + 1` 104 → 100, `x% + 1` 1.02 → 0); `mod` has the + precedence of `*` and `/` (14.0: `10 / 4 mod 3` 10 → 2.5); implicit + multiplication after a signed constant division (11.0: `-1 / 2 x` -0.25 → + -1 for x = 2); empty true branch of `?:` is an error (15.1.1); a delimiter + after hex/bin/oct literals (15.0); + - tolerances: comparisons use `relTol` 1e-12 and `absTol` 1e-15 instead of a + relative 1e-12 and an absolute 2.2e-16 (13.0: `x > 0` for x = 1e-15 true → + false, `abs(x - 0.3) < 1e-15` for x = 0.1 + 0.2 true → false, `isZero`, + `isPositive`, `compare`); `isInteger` with tolerance (15.1: + `isInteger(0.1 * 3 * 10)` false → true); `mod` round-off (11.11.1: + `0.3 mod 0.1` 0.0999… → -5.6e-17); + - functions: element-wise `sqrt`/`exp`/`sin`/… of matrices removed (11.0), + `size` returns an array and subsets keep the shape of the index (15.0), + `print` uses one-based indices in expressions (11.11.1), `typeOf` of a + function is `function` (11.0), exact `combinations` (10.2), `mod` with a + negative divisor (12.1), US liquid units (13.0.3), new functions and + operators (`??`, `?.`, `isFinite`, `cumsum`, …). +2. **fraction.js 5** (Math.js 14.0): Fractions hold bigint numerators and + denominators, so they are exact at any size (0.1.17: doubles, exact up to + 2^53); numeric strings keep every digit; the JSON form writes `n` and `d` + as strings; `valueOf()` overflows for large parts (the engine's `toFloat` + converts with bigint, correctly rounded); long sums grow without limit (the + mean of the probabilistic sensitivity analysis, handled in sd-computations). +3. **The engine adapter**: only `toFloat` (bigint conversion) and the rejection + of Unit results in `toNumber` / `eval(expr, true)` (as in 0.1.17). The + arithmetic helpers are exact (no rounding policy). + +Expressions of the 16 saved diagrams in the test data of SilverDecisions and +sd-computations give the same values in both versions; the differences above +come from constructs those files do not use. Syntax alone does not show all of +them: tolerances, `isInteger`, `mod`, matrix functions depend on the values, +which change with sensitivity analyses and random draws. + +## Proposal: old diagrams + +### 1. Semantics version of a diagram + +- Saved files carry the SilverDecisions version (`"SilverDecisions": "1.2.1"`); + all releases up to 1.2.x use the engine 0.1.x (Math.js ≤ 9.5). A file without + the new field below is a **legacy** file. +- New field written on save, e.g. `data.expressionSemantics: "mathjs-15"` (a + name of the expression semantics, not of the app version, so that a later + Math.js upgrade can add another one). Jobs in the job repository store the + semantics of their data model together with the job instance. + +### 2. Classification on opening a legacy file (before any recomputation) + +Every expression (global code, node code, payoffs, probabilities, text +templates with `math(...)`) is parsed with the **legacy parser** (Math.js 9.5 +`parse`, a separate bundle loaded only for legacy files) and with the current +parser. + +- **S, syntax only** (precedence of `%`/`mod`, unary percent, implicit + multiplication): migratable. The legacy tree printed with all parentheses + (`node.toString({parenthesis: 'all', implicit: 'show'})`) is parsed by the + current parser; the migration is offered only if the two trees are + structurally equal (node types, operators, names, constants). Expressions that + parse to equal trees in both versions need no change. +- **F, changed functions** (matrix element-wise functions, `size`, matrix + indexing, `print`, `typeOf`, `mod`, `isInteger`, `combinations`, units, …): + found by the names in the tree; not migrated automatically (the result + depends on the argument values and types). +- **T, tolerance-dependent** (`==`, `!=`, `<`, `<=`, `>`, `>=`, `compare`, + `equal`, `larger`, `smaller`, `isZero`, `isPositive`, `isNegative`, `sign`): + value-dependent. Option T1 (engine-wide): keep the 0.1.17 comparisons by + creating Math.js with `absTol: Number.EPSILON` — reproduced: it restores + every comparison case of the table (not `isInteger`, `mod`, `floor`/`ceil`). + Option T2: no change, flag as F. +- Parse errors in one version only (`c ? : b`, `??`, …) are reported. + +### 3. Dynamic check (value-dependent differences) + +For a legacy file the base case is evaluated with both engines (legacy bundle +and current) and all values are compared: diagram and node scopes, payoffs, +probabilities, the computed values of the current rule, interpolated texts. +This finds differences the syntax cannot show (a comparison at 1e-15, an +`isInteger` of a computed value). It covers the evaluated values only: a +sensitivity analysis or random draws can reach other values, so expressions of +class F/T stay flagged even when the base case is equal. + +### 4. Before recomputation + +The file is shown with its saved computed values (marked as computed by +SilverDecisions x.y), not recomputed, until the user decides: + +- nothing found (no S/F/T findings, equal base case): open normally, mark as + current semantics on the next save; +- only S: offer the migration (list old and new text of each expression), then + recompute; +- F/T findings or base-case differences: a report (node, field, expression, + value with 0.1.x, value now); choices: migrate S and accept the new + behaviour for the rest, keep the file read-only, or cancel. No silent + recomputation. + +### 5. No repeated warnings + +Accepting writes `expressionSemantics` (and e.g. `migratedFrom: "1.2.1"`) into +the diagram; the next save stores it and later openings do not warn. Without a +save the warning appears again (the file is still legacy). Jobs: results of +jobs stored by 0.1.x are data and are shown as they are; a re-run uses the +semantics of the current data model. + +### Costs and open points + +- The legacy parser bundle (Math.js 9.5) is loaded only for legacy files; size + and whether `parse` can be bundled without the rest of Math.js 9 are to be + measured. +- Templates of texts (`${...}`, lodash templates with `math("...")`) need a + parser of their own to find the expressions. + +## Decisions needed + +1. Runtime requirement ES2020 + BigInt (Math.js ≥ 13). +2. Comparisons: T1 (keep the 0.1.17 tolerances with `absTol: Number.EPSILON`) + or the Math.js 13+ tolerances. +3. Handling of legacy diagrams (sections 1-5) or an alternative, before a + SilverDecisions release with Math.js 15. +4. JSON form of Fractions with string `n` / `d` in the job repository (read + back from 0.1.17 records works, the reverse does not). diff --git a/src/expression-engine.js b/src/expression-engine.js index 4b5e89f..b361034 100644 --- a/src/expression-engine.js +++ b/src/expression-engine.js @@ -13,82 +13,61 @@ sdRandom.functionNameList.forEach(fnName => { } }); -// Math.js 14+ (fraction.js 5) computes Fractions exactly with bigint numerators -// and denominators: a sum of many evaluated (float) values, e.g. the expected -// values of the probabilistic sensitivity analysis, grows to thousands of digits, -// gets slow and its JS number (valueOf) becomes NaN. With Math.js 9 (fraction.js -// 4) numerator and denominator were doubles: about 16 significant digits at any -// magnitude. The arithmetic helpers therefore keep a result exact while its -// numerator and denominator have at most MAX_BITS bits, and otherwise replace it -// with the first continued-fraction convergent within a relative error of -// 2^-RELATIVE_BITS (about 5e-20). Small values keep their magnitude; results -// are exact up to 64-bit numerators and denominators (Math.js 9: 53 bits) and -// otherwise about 2000 times more precise than a double. The bounds keep the -// cost of long sums close to Math.js 9. -const MAX_BITS = 64; -const RELATIVE_BITS = BigInt(64); +// Fractions of Math.js 14+ (fraction.js 5) hold bigint numerators and +// denominators. fraction.js 5 valueOf() is Number(n) / Number(d): it rounds n and +// d first and gives Infinity or NaN when they are beyond the double range, +// although the quotient may not be. toFloat therefore converts with bigint: +// the result is the double nearest to n / d, ties to even (IEEE 754 +// round-to-nearest), including subnormal results and overflow to +-Infinity. const ZERO = BigInt(0); const ONE = BigInt(1); - -function abs(v) { - return v < ZERO ? -v : v; -} +const TWO = BigInt(2); function bitLength(v) { - return v === ZERO ? 0 : abs(v).toString(2).length; + return v === ZERO ? 0 : (v < ZERO ? -v : v).toString(2).length; } -function roundFraction(f) { - if (!math.isFraction(f) || (bitLength(f.n) <= MAX_BITS && bitLength(f.d) <= MAX_BITS)) { - return f; - } - // f.n > 0 here (zero is 0/1); convergents p1/q1 of f.n/f.d - const x = f.n, y = f.d; - let p0 = ZERO, q0 = ONE, p1 = ONE, q1 = ZERO; - let n = x, d = y; - while (d !== ZERO) { - const a = n / d; - const p2 = a * p1 + p0; - const q2 = a * q1 + q0; - p0 = p1; - q0 = q1; - p1 = p2; - q1 = q2; - const r = n - a * d; - n = d; - d = r; - // |p1/q1 - x/y| <= 2^-RELATIVE_BITS * x/y, cross-multiplied - if ((abs(p1 * y - x * q1) << RELATIVE_BITS) <= x * q1) { - break; - } +// x * 2^k without intermediate overflow or underflow (k may exceed the exponent range) +function scale(x, k) { + while (k !== 0) { + const step = Math.max(-1000, Math.min(1000, k)); + x *= Math.pow(2, step); + k -= step; } - return new math.Fraction(f.s * p1, q1); + return x; } -// JS number of a Fraction. fraction.js 5 valueOf() is Number(n) / Number(d), -// which is Infinity or NaN when n or d is beyond the double range (about 2^1024) -// although the quotient is not; such Fractions are divided with bigint to 64 -// significant bits and scaled by a power of two. function fractionToFloat(f) { - const nBits = bitLength(f.n); - const dBits = bitLength(f.d); - if (nBits <= 1000 && dBits <= 1000) { - return f.valueOf(); - } - if (f.n === ZERO) { - return 0; - } - // q = floor(n * 2^-e / d) has 64 or 65 bits - const e = nBits - dBits - 64; - const q = e >= 0 ? f.n / (f.d << BigInt(e)) : (f.n << BigInt(-e)) / f.d; - let v = Number(q); - let k = e; - while (k !== 0) { - const step = Math.max(-1000, Math.min(1000, k)); - v *= Math.pow(2, step); - k -= step; + const n = f.n < ZERO ? -f.n : f.n; + const d = f.d; + const negative = f.s < ZERO; + let v; + if (n === ZERO) { + v = 0; + } else if (bitLength(n) <= 53 && bitLength(d) <= 53) { + // exact integers, one correctly rounded division + v = Number(n) / Number(d); + } else if ((n << BigInt(1022)) < d) { + // subnormal (below 2^-1022): round n / d * 2^1074 to an integer, ties to even + const t = n << BigInt(1074); + let q = t / d; + const r2 = (t - q * d) * TWO; + if (r2 > d || (r2 === d && (q & ONE) === ONE)) { + q += ONE; + } + v = Number(q) * Math.pow(2, -1074); + } else { + // q = floor(n / d * 2^k) with at least 55 bits; a set lowest bit for a + // non-zero remainder (sticky bit) lets Number(q) round correctly to 53 bits + const k = 55 - (bitLength(n) - bitLength(d)); + let q = k >= 0 ? (n << BigInt(k)) / d : n / (d << BigInt(-k)); + const exact = k >= 0 ? q * d === n << BigInt(k) : q * (d << BigInt(-k)) === n; + if (!exact) { + q |= ONE; + } + v = scale(Number(q), -k); } - return f.s < ZERO ? -v : v; + return negative ? -v : v; } @@ -131,19 +110,19 @@ export class ExpressionEngine { } static add(a, b) { - return roundFraction(math.add(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return math.add(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); } static subtract(a, b) { - return roundFraction(math.subtract(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return math.subtract(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); } static divide(a, b) { - return roundFraction(math.divide(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return math.divide(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); } static multiply(a, b) { - return roundFraction(math.multiply(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b))); + return math.multiply(ExpressionEngine.toNumber(a), ExpressionEngine.toNumber(b)); } static round(a, places) { diff --git a/test/contract/fixtures/README.md b/test/contract/fixtures/README.md index 72ceb79..ca0f5c4 100644 --- a/test/contract/fixtures/README.md +++ b/test/contract/fixtures/README.md @@ -16,3 +16,8 @@ Normalization: global code that calls `random()` cannot be repeated, so for such files the saved `expressionScope` (the result of that code when the file was saved) is used as the diagram scope instead of evaluating the code again. No other value is changed or recomputed from the current implementation. + +`tofloat_cases.json` (not a diagram): Fractions and the correctly rounded double +of each (ties to even, subnormals, overflow, random large Fractions), generated +with Python's `fractions.Fraction` by `tofloat_cases.py`; regenerate with +`python3 -I test/contract/fixtures/tofloat_cases.py > test/contract/fixtures/tofloat_cases.json`. diff --git a/test/contract/fixtures/tofloat_cases.json b/test/contract/fixtures/tofloat_cases.json new file mode 100644 index 0000000..4b9d957 --- /dev/null +++ b/test/contract/fixtures/tofloat_cases.json @@ -0,0 +1,1133 @@ +{ + "generator": "test/contract/fixtures/tofloat_cases.py", + "cases": [ + { + "n": "0x100000000000008000000000000000000000000000000000000", + "d": "0x100000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000000", + "note": "1 + half ulp: tie, to even (1)" + }, + { + "n": "0x100000000000008000000000000000000000000000000000001", + "d": "0x100000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000001", + "note": "1 + half ulp + tiny: up" + }, + { + "n": "0x100000000000007ffffffffffffffffffffffffffffffffffff", + "d": "0x100000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000000", + "note": "1 + half ulp - tiny: down" + }, + { + "n": "0x100000000000018000000000000000000000000000000000000", + "d": "0x100000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000002", + "note": "1 + 3 half ulps: tie, to even (1 + 2 ulps)" + }, + { + "n": "0x100000000000017ffffffffffffffffffffffffffffffffffff", + "d": "0x100000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000001", + "note": "1 + 3 half ulps - tiny: down (1 + 1 ulp)" + }, + { + "n": "0x10000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "d": "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000000", + "note": "1 + half ulp: tie, to even (1)" + }, + { + "n": "0x10000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "d": "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000001", + "note": "1 + half ulp + tiny: up" + }, + { + "n": "0x100000000000007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "d": "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000000", + "note": "1 + half ulp - tiny: down" + }, + { + "n": "0x10000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "d": "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000002", + "note": "1 + 3 half ulps: tie, to even (1 + 2 ulps)" + }, + { + "n": "0x100000000000017ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "d": "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000001", + "note": "1 + 3 half ulps - tiny: down (1 + 1 ulp)" + }, + { + "n": "0x100000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "d": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000000", + "note": "1 + half ulp: tie, to even (1)" + }, + { + "n": "0x100000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "d": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000001", + "note": "1 + half ulp + tiny: up" + }, + { + "n": "0x100000000000007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "d": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000000", + "note": "1 + half ulp - tiny: down" + }, + { + "n": "0x100000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "d": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000002", + "note": "1 + 3 half ulps: tie, to even (1 + 2 ulps)" + }, + { + "n": "0x100000000000017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "d": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000001", + "note": "1 + 3 half ulps - tiny: down (1 + 1 ulp)" + }, + { + "n": "0x111f249dd9ddb588cb812d850da7fe92e1f5b1", + "d": "0x111f249dd9ddb4ffd25c3eb620000000000000", + "expected": "3ff0000000000000", + "note": "tie (2^53+1)/2^53 scaled by 3^k, to even" + }, + { + "n": "0x111f249dd9ddb588cb812d850da7fe92e1f5b2", + "d": "0x111f249dd9ddb4ffd25c3eb620000000000000", + "expected": "3ff0000000000001", + "note": "above the tie, scaled" + }, + { + "n": "0x111f249dd9ddb588cb812d850da7fe92e1f5b0", + "d": "0x111f249dd9ddb4ffd25c3eb620000000000000", + "expected": "3ff0000000000000", + "note": "below the tie, scaled" + }, + { + "n": "0x16739ffe90b4bc3237d5508bbcff743883b7a52a55acbeb77317fcd910b842056bb1048dea5b2a45da521e6037c9ec11385e6d03c181e2f35d04307d34e67dc5c2671", + "d": "0x16739ffe90b4bb7e9ad55c0617237f61d8d7747139b1aff0b774730b8338bc49c818a8742478dc05150e7d3c70e9c368c474897c73669ccfb8b84ce20000000000000", + "expected": "3ff0000000000000", + "note": "tie (2^53+1)/2^53 scaled by 3^k, to even" + }, + { + "n": "0x16739ffe90b4bc3237d5508bbcff743883b7a52a55acbeb77317fcd910b842056bb1048dea5b2a45da521e6037c9ec11385e6d03c181e2f35d04307d34e67dc5c2672", + "d": "0x16739ffe90b4bb7e9ad55c0617237f61d8d7747139b1aff0b774730b8338bc49c818a8742478dc05150e7d3c70e9c368c474897c73669ccfb8b84ce20000000000000", + "expected": "3ff0000000000001", + "note": "above the tie, scaled" + }, + { + "n": "0x16739ffe90b4bc3237d5508bbcff743883b7a52a55acbeb77317fcd910b842056bb1048dea5b2a45da521e6037c9ec11385e6d03c181e2f35d04307d34e67dc5c2670", + "d": "0x16739ffe90b4bb7e9ad55c0617237f61d8d7747139b1aff0b774730b8338bc49c818a8742478dc05150e7d3c70e9c368c474897c73669ccfb8b84ce20000000000000", + "expected": "3ff0000000000000", + "note": "below the tie, scaled" + }, + { + "n": "0x100000000000008000000000000000000000000000000000001", + "d": "0x100000000000000000000000000000000000000000000000000", + "expected": "3ff0000000000001", + "note": "(2^200 + 2^147 + 1) / 2^200 (review example)" + }, + { + "n": "0x37a0790280d2f3d3fe01dfdfa997963442ea39ab8daa5014b22ea3b6aedb19210f8f7462987c46f2c2733de4a4b89e43587788fcba28fe3c3ba1030e0b4c2b301525eb9739fc51af291478abfb8736c10c186472be04f110d492400000000000000000000000000000000000000000000000000000000000000000000000000001", + "d": "0x3f1", + "expected": "7fac3a1654ff2649", + "note": "(10^310 + 1) / 1009" + }, + { + "n": "0x1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "d": "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "expected": "0170000000000000", + "note": "(2^1200 + 1) / (2^2200 + 1)" + }, + { + "n": "0x1", + "d": "0x80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "0000000000000000", + "note": "half of the smallest subnormal: tie, to even (0)" + }, + { + "n": "0x1", + "d": "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "expected": "0000000000000001", + "note": "just above half of the smallest subnormal: up" + }, + { + "n": "0x3", + "d": "0x80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "0000000000000002", + "note": "1.5 x smallest subnormal: tie, to even (2 x)" + }, + { + "n": "0x5", + "d": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "0000000000000001", + "note": "1.25 x smallest subnormal: down" + }, + { + "n": "0xfffffffffffff", + "d": "0x40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "000fffffffffffff", + "note": "largest subnormal" + }, + { + "n": "0x5ffffffffffffd", + "d": "0x180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "0010000000000000", + "note": "between the largest subnormal and 2^-1022: tie, to even (2^-1022)" + }, + { + "n": "0x1", + "d": "0xc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "0005555555555555", + "note": "1/3 of 2^-1022 (subnormal, non-dyadic)" + }, + { + "n": "0x1", + "d": "0x12d8dc1d56a13cc9abd94f06861b833eade87dad8aa62c361c45297ed7ce20501b8c88b056cd6f94c4142bb31b48adac9ef37a52d21cc71880a3c01134060260fa8734c9469130f0d8ef3b8f12e33f44c667d142fe9a0763d71d3f5fb647717e40000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "0000000000000000", + "note": "1e-330 (subnormal)" + }, + { + "n": "0x1", + "d": "0x1b4ec7f91973ff3cb1ccf26fbc177c38db6e54582de258ff5190b8bc150afadb38030e5d0b7bf50120e5c0e6ad18e629853ce9946a0b2eccf6cfde0af8634bf8dc5a427fe4236ce5959baa0e776cdb47c4813e1e0c75ca271e1d059e08e4996aa4193a48ad3a24633ebdb32e9f0b824377e2fa9c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": "0000000000000000", + "note": "1e-400 (below the range: 0)" + }, + { + "n": "0xfffffffffffffc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "d": "0x1", + "expected": "Infinity", + "note": "midpoint MAX / 2^1024: tie, to even (Infinity)" + }, + { + "n": "0x2fffffffffffff3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "d": "0x3", + "expected": "7fefffffffffffff", + "note": "just below the midpoint: MAX" + }, + { + "n": "0x2fffffffffffff400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "d": "0x3", + "expected": "Infinity", + "note": "just above the midpoint: Infinity" + }, + { + "n": "-0x2fffffffffffff3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "d": "0x3", + "expected": "ffefffffffffffff", + "note": "negative, just below the midpoint: -MAX" + }, + { + "n": "0x1b4ec7f91973ff3cb1ccf26fbc177c38db6e54582de258ff5190b8bc150afadb38030e5d0b7bf50120e5c0e6ad18e629853ce9946a0b2eccf6cfde0af8634bf8dc5a427fe4236ce5959baa0e776cdb47c4813e1e0c75ca271e1d059e08e4996aa4193a48ad3a24633ebdb32e9f0b824377e2fa9c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "d": "0x3", + "expected": "Infinity", + "note": "far beyond the range: Infinity" + }, + { + "n": "-0x58e354b1f5dab20f4aa46764e29b77f67a73f91da3a125", + "d": "0x544651d4cc381c4efba6b88aa3", + "expected": "c4f0e038a6948f64", + "note": "random" + }, + { + "n": "0x6dafae9302986547b52a0a119fa2c1be1e007df311b65e0acc655c88dd1c8af152f872fbb1ed0ed3cd1e901bf3a517ef8389c1a7157346352fef77de3f69506bf97c05a8330d10058604a9fc7c87be8e5b3e922e4e4a1174a44887d115906d658e2e6d7347079d53248e08f7199d6f6e5148b", + "d": "0x3448a11dd79d5a4f412961c75951ef03fb06e5557468ede4c32916369ad49d83d2d33abc3b2b34c280e26e207780255c44607645355fd934eae3d122eda5c8f", + "expected": "5980c88275634c65", + "note": "random" + }, + { + "n": "0x15635fd128e73e2c71b97e0f727480af", + "d": "0x37262ced5d895e3e73c0c99dedd2886bdc2642bfbbafbaf9201d4e6c4bb0b7e1a5010a2d85d8044822b5c0bec0f23fb8ade27bd2a3b868f345a2f8004195e422757df09a0b7cbf7c73cb018066d2982d768d8fbceaab62070a2485598ed94e40b56056f0701b9e98f5751a58feb72648ff7f328b8f2f34a6e4cc964fc5", + "expected": "0958d21dd4deff53", + "note": "random" + }, + { + "n": "0x52d36ab6b37aeb01e3d693614da9e0f7409ebbbb2ed4808d1f53eb8c0f0f90f35709e405a4f8bd08c6caf04cb25dfd526afe5eddb69a0745865e8784f9e9b29664c8765d49ef6429df8744c4a65187462aaf7a4a0965e9aa86c74967c880b74c3708d722fa2c58cf2d9efb0176f7f4587bd38a8435b20681", + "d": "0xed086ba0285ce52b12816ca66090985fe84e3c4a5bdf543ef827e5fd5fba5913dd74162ad849c812d81852b2a1dd36ee9a66a0725f51b4c54715b879e2017d1306fbe0bc38b4e54d58be046b0c20b3a7fc1f79035325471ff5bc51a9bae77ca3c6295c77c08e8765adb0bf2a22b1640956f02824fdf4e2166ede1870978513033587e8f7ec859194258220bac4cb7ab25", + "expected": "33965d05ccd30b12", + "note": "random" + }, + { + "n": "0x7942257832e8bae95c9281a0bf5d6ef2a454053508fd1b3664428edc5e91fc8ee2532b200f29dc2fa112bda9c4c7b912f11980996fd9a502e5e4db0b6c35a209175b880f74ea00d789682881f4bade7562eeed945a79972ed9eed2b4ae1c0719c485d0cfe811d9b119c661ca388afc72a858132839bb23ab2e3f179", + "d": "0x12d5312cf99c477bea704bdfb51cacd8ff1aeb085bc4886df3f5d014fec2f084136b0645002720968ccd0ebbe9683336d81a260e4feaaddfdeb1c077b56bc09e3355fa5a524391155d1aa247141b427ae538783dfe5797d58b740f1f886e675c4258977b76b68dc1b0cf76c6a7b8042a2c45ab2c3f00e7c9505c09afdc7a675c6a555f8b169aecc874154f78f685b", + "expected": "3699c13723779f1c", + "note": "random" + }, + { + "n": "-0x96536a7005b2e768df6026fcedda3193b2b7c5251bf99735ec7d1e13bef4f9047833ad4a401c8d456941362900b3a25df0161ffd014bf31a9a9f41338b11c8758128a46c4b31f6f40af477e0165ee99ebbfb4646619caacd14aa73aca3188b2fd96a6516b8de2bc5152a1013c045a9f34a7d74505328854887f", + "d": "0xf2709bed0842c9321865cb7643a62de1afad55ca380dc5bc9517ce896280d4fdeacbd9efd11", + "expected": "e9e3d77bb5fdf1dc", + "note": "random" + }, + { + "n": "0x268612f766e4e29340f8a411dda641867d9102b09f92a0f6475d8dbb02b86712b12b4851122b04ec3c093c89d4beedd012fa0e699ba225d40e4e7ebe69515b85e284c6c677fb96143d08eb4d7081cd7256ce34999247e3040b05e35f28689ce604c9875b57", + "d": "0x993c962406d2c99d8a4889f37c5a7282774e72e7de93defd", + "expected": "665016f0d2531a88", + "note": "random" + }, + { + "n": "-0x755ecc724867f10a87f8ef00c1eac52fe0e26a9dea2071592b55b4b5375e62b8df5acd85c25dffe77c096a1f32e5fc95ca489f0bac664598033493c8518d41fca5d08d0ab460ad263e4562e093afc78b5f6e7014b3d5a522ee1276ac915e7bd195a9f52097d12639ebbe49e45136dc6406cfdcf5247da49f0bc1108adedacc5720ff", + "d": "0x18d0c4f25a019d9f2ac3c2ef1e494d8f3bae82d1985ebc9590f04e0ab2a5ab2211076bc6a502d9c4d5459e327dab041f56831b3c9b29aa45d7f27c69f50228b9955727568ca8a5fb9e53953d3ef66aaa33614f683043ee625eafb2902b7712f01a86443ddd65f374ecdaba95b205182e9e655697d6b823525bc6aefb2dd658ad7", + "expected": "c0d2eb3ad514b7ab", + "note": "random" + }, + { + "n": "0x29ca5145bbff8da46d58793f599145a13ef3492ef850b9b089647b37c60e5f45932addffab55bd6b4b", + "d": "0x8ebb19c7152d7034516596c97ce9c84b6bdb8c914a7093a386658cb4addb3292cd8232ac6c934b264ee7f9c8ef362ec73eb992c92090b93f208ce9099bb2bd57e5aeb2973df62ff11537041268b8e5", + "expected": "2cd2bd163102b28f", + "note": "random" + }, + { + "n": "0x6b8520091071cbb5d0670edcaa42cda8580086bb8f907e89722e8226b5199e72015c64970fde37355b25cc7402a89d5b575737d9c91fe84dbd2abf2447a2ce7312bfbf20f038c115415a6c638e9d41c4867fac5f3a6442d04572a4e6c189f80c5b00abbbf46245963f93046885f68a6e540219d01156134fd2bba46681792554041b33b77414502825c074f59bbcc0563cceb1", + "d": "0x7a2266824caf86f46114cb7410bd9417afe071e83b92f5c4f6c1fca21e89ae2b30e6599d45eb2eb8691148f79eb885758045e20b515200e2ef1a73a1369790ce3ba2ca46471a62fe38e01586510c9", + "expected": "622c2bc2f67bc19d", + "note": "random" + }, + { + "n": "-0x112e1b5c15e3a7e70ddc1858faff436dc6457d75183d63889241e06f2d3bdb5", + "d": "0x1d5fffa2667e19579e0d3681a1609801ee99c2a0abc628479bb0cbe95fad762129", + "expected": "bf22b7214e3f105b", + "note": "random" + }, + { + "n": "0x781017c7e5b5d9a47f7a7b3e5b8288b849fe856d32f9709aeda67ef", + "d": "0xae8096a1129f50c84805148971c4e2829dfd5d6b2ff9d6c6b90602acc6bad9e2c42fb53e46bdfab04c3f0047e19b", + "expected": "36a604580c99bb00", + "note": "random" + }, + { + "n": "0xf4e7a794360184689b048830636a8b547d047bc669fe9cc16b342505be38288bc2f780ce7a1ea1a5a70def9f44e2b842795c194abced8229b9cb379ae73351db", + "d": "0xfa91d29560fa4c63a5ae7eb40caa3e65f8a6c63c10653b7f2adca57da1c816c645f13329448ca24152b75a59c3dcdb310153d53e324adae307305f774ea518b427c7336e10b4f124166d5e2c036ec5775b7d9c741490e1", + "expected": "346f46cceba70915", + "note": "random" + }, + { + "n": "0x95098a21fb807c015348f87d51224889b84e3b8132c55b38cf11b09c7893ea32c5326b65c18e232dbdcf9817ca90fd00b16a72152711fbe5b3fdc9f756d8b7a377cae96c531a2f59c0272e18a250b6f889a8768bf7bb1f284b258f92109eb5312c3c3054ca99003864fb53c7e242f56ef7", + "d": "0x15158f13632554a34b9c0a5748331898738623bb2d2c7be1f8f111e600cec5da65f58e72dce476b43cda309f35db5fbd01d312467632332baa9b44c2cc26cae8769a963d68bc6d3b9a9dd37c963ac458a24449d8bb2a17aef3c9c858d8600f216ca205924d701b210cc955b0ccaac837599ecfa1c003d11", + "expected": "3cdc46504f8aa9c4", + "note": "random" + }, + { + "n": "0x157a07ea78ad4a6df34c4f8e761fa8e725f8c450afbc25891f94b2bc126f4e714a410e1049739c6f37853e96ca31d19", + "d": "0x3f01d3e5621179d8b8533ef2dea7aed8ad33c5332b89569ca88f69", + "expected": "4a15d0ab1a105ad2", + "note": "random" + }, + { + "n": "0x853051fd3cb0d14986840a3ca6e818cec69449536fa6edb6646e7539cb01c5f82a1697df2e72772164f761cb38af6b328e24af24f614fc696ce5e658267aa2b7815782a74e488a49229b41567363403a0aaaaa2a207b7dfc974b17d4d131553447bf404c867ff66c905afdb88d7d66c93b3e5a330c1970fcdad1ea4f0830a23c7b2551ea7ba105ac53284d75ccde5a993", + "d": "0x719c03025a3377aafefcd210c8fe6b17defb853951cba85909f101d1e7f004114dbe0e3791f95a2b97dc9d39edae066a0ef9829f2545f58f317e43ebbe0fc45ce8038b8abefcde1a5c51a6bb72664663a640cfb4b4036f37f08b35ae2cf5659aa7f551cdca4155e2ef44401299ba64fcebce5e7fc8d5a94767269b5f7602bba16cecb5e7474cd764731bd", + "expected": "42f2c1e680f349bc", + "note": "random" + }, + { + "n": "-0x10d4449daaa2190c7be63fb6e79b3260eb9e237129421abb0cba5e96cd8dda3bd7946266e9d", + "d": "0x59f67a214b885fc9343a4194b6d1d2e2c302703e0d3344775158d1602d0e62019f94f070722c6891bcbdde4f9a41c3a9ddf5cced9d340138e4ea4d35b03045938a5abb90dd557d64df02c9f67bfeb166d6c9ab6f27374d1373660b1cfa2f025640e5baad17bb7", + "expected": "9f47f1d91f219f16", + "note": "random" + }, + { + "n": "-0xcc9eba221c561eb483c28c3b59aed305a9869f801063d0d1658de4d6de95e7ac380fbd18a1d523329171b2e56c48dcce4a2dd9f9572cc29f7519b2d29b39f0f75dee94f195c3598d4dd310d8d427be1a75b3cb44f7d81749f66b186de0f1421d7d798ad82bacc61ed3f853349d1ddeeb9d63c6a7a63be9627a118fe83c00149d5ad3301", + "d": "0x2919cdfe69b343c6e0a437c7524e1996d56afb6ec2ab50f6b54c310add13049aaa9655", + "expected": "f053e9f9f98ca799", + "note": "random" + }, + { + "n": "0x150b9f066b3d6c152479b13670e5374d1b1a65d7b1e9866c876cfc2e4094879d2d8ca6e7e481a8ed3ab916cd05732d11054f3374e3d329ee21df354251995840037387f73ed4048a473ea6dfb4e270da33dafa8bd4686d7cc9a64171a3bbbad57eacbf75", + "d": "0x1f8ec857b15fe29d761c695598e4349b473aacb8aed5f229d88f46deecee8e30c60534f9ef93bd2dc8b7f9433dac1a94971db06bacf6edd4b8219d42c21dd9930db9a0d69c55eb54be8301735ec5591a8f3400832e80ffba0162d4f54ad4eb0e09d70e09c75b74399b1febb94155f0f0bda41f3", + "expected": "3825571fccb291ea", + "note": "random" + }, + { + "n": "0x24875ca5d8ef59f610713e9ebb1d3a20bc6cfc974303dd15b3fd6d4bcfd393e1e27f0ca924f31199c4ef868ffff1ffed96af822b3ac4774d16711e7c64eb1ad785f5c2ce715e6415ea9f07a63d8bcb76e9f73368b", + "d": "0xb76425ca299baa3f10bc08ffcafa67abd3e81396511b0a165ab770f0dd72b0394dc031eb26aa9a0ca72dd9ae274099e429", + "expected": "51897ee40355408f", + "note": "random" + }, + { + "n": "0x17a278496e369663a7f860d47719e7989e9b5b6b71359e5c382dc76c1678cbc629a3025be0494c940d6ef178b53d6cc8a1a0019b845ebb53a3ba477dfe29ff936ee811645300c1f8c18923ea1e7cb9da6dd690f19d60767d691b0b52ae09aa20ef86daf96282904e7240febcb4aa3c6e3f49f25d02761ec0b27775f6ad384c813", + "d": "0xabd82107621c8f6665bcf05ffdbebf74ad3a25d6f1129c6e349dce9a6c089906c32d38936400e3c68def7683a91de9f869b50138f8833c4ee433e550db2cb69ca8a56e11f11a6b1300d543290d8c2198a5dd", + "expected": "57019ac1911f08d6", + "note": "random" + }, + { + "n": "0x75497e8beaa8611afa3053a865b60a2b", + "d": "0x95f9cd", + "expected": "4669067918f26b02", + "note": "random" + }, + { + "n": "0xa8297a1e68c34f46e677bbeb2766257c999e6035577a3c04dc1c9ea96d4a230239c1197ae150f130f40cb9492aac1f627a0fbf4696aa2fd4cd5f379cc0a7a31ca6848c04ae75726657248b31d9b2c399b16d579051142a342b17ea3cbcd723514413c0e5e696df20d10b0cfb7f29441bcfbe144635e8d", + "d": "0xea7f647dd516a191577b531ecf8f2b895f6cce6291626ebd1e9daccbfedb4a261ac2bd64a00fe1b3816595fc8aad53520c3be48c66e0d4d7a504a8b576c7ca2efe7af9911ae9d78194fde6442b59e7f7516342361d0bea23de6dae1ee0bb50efdcc23c1d1be3227b30406ef72474f1e1473c2373e081d2d2fac1eb8ce8c67c109b2913044bc43f79c2a7", + "expected": "3626f29d6d056a89", + "note": "random" + }, + { + "n": "0x936ab3bd17bc6e05fda9a43ac8b14f8102d7", + "d": "0xf23be432dc2d58e4c0234723e946645b3adcf0c314c3fa5ff412b688f529b356a734193fd7b8011d24fd44836e5bf22d8e2e796223897b75444b2303bf3103f2fa68a26670b6905a9eded3db07a306811af219b4bf79f5d38a68f995bb5748d941b0ea7577d7dfaee1403e4b6f5c884cc2fef", + "expected": "0fa3796c082cdb04", + "note": "random" + }, + { + "n": "-0x330fcafc6c48905ca1c4028922b68d319b335c7223ef089b63649bd4d902b75f1b5b2d7a321ed729497e2a82c39e05722521cec54fdd66e7f8181697000eb2bc8f6972a108b072b136e0749c244e79367da8d", + "d": "0x2930e56ae9a4170b36ecbee8b67", + "expected": "e273d588950d454e", + "note": "random" + }, + { + "n": "-0xcd5c044784fecba55aa9cd8d4a0afdc5580f5e825adc9", + "d": "0x3b618c2eacb8d7b48309f16e3bb548295dabff9f08516dc60ffcd466bd80ab1104d83b134d0d0347e1fc59ab239af77fa232dd40fbf718fb5aa6fcbd0d7523cdc11012212200184a2fd288e9c16ff4e59b4cfb9c6c7272de95fe969b8d5c51144f266054863b1710c4380be7", + "expected": "954baaaa98cea855", + "note": "random" + }, + { + "n": "0x5a7a6dab32c5909eaa2bd1da814e4cf6f0e5568f8b580d97a9f7947faa772657ab6a96e8f1142c7db2108b8f9b9c35980963ae2cc59a84a65e67e8c946c1196212deca1a8c543bc5390f8e3a1ed3409420beefbef822e7346fe0319e8e7f641f45201bb0918f3b8222c0170b93f318f79e6b1649a732138665ae9479fbc8e318199b", + "d": "0x2ea19a2865118f80227a02b3582a04f54b5d51fdc5b6f563bd698d9d5b43e2762e10b3b7be72235655e7eacf9c6ba3eb58526a465b2a7abd87658899e6fe1fc0f2bacaeb3eedd2ed75ad9f29f6d8eae82ecbe110f5720f23b9a86e9e11fb6b", + "expected": "517f0b6f21919efc", + "note": "random" + }, + { + "n": "-0x13b9efe270f1910900db238bba20c673211d4ef80af028558b19e5078e3cf62d819cba6397", + "d": "0x80d9a0032a2d7daa437aae615836ca13bc04360ba2f3b595760aeeeab6bbc7cfdcfd9b170ca924cf936d865c91fae4a134c7e06ccf0acd0986af749defed30c9a75d6bf3b75fe9d59777d43f210813e4913b9ae1f69dcf01a9cf215f0344ab0b01", + "expected": "a1c3989ea638ccbc", + "note": "random" + }, + { + "n": "-0x64659ec4994809603bb6eb1b4507544aac7f88afaaf7308b160097e69d", + "d": "0xf78b40392f132fb41ee8c36d47be4dc5c1959d466b5f38cf9854578dd3334c2c0b466f440f37f2079c6f50854f8cbce0a7f34dc1176b", + "expected": "b359f4e54511d38b", + "note": "random" + }, + { + "n": "-0x1af936b22617545f43", + "d": "0xadafc19241b930d98a37282ce524f50e7a34d4b31d4934c5d3085cebc2de9078a00628cb91f311eb3a9044a59dbfebacee514d8a38dbbf898219aa14cc7cd8ff5024243d45cc8f35a9759a478b11e860f5ca4ab76a76791ce7016ba06b6d7b1ef2bc6cc85bc8ac8eb0f4d", + "expected": "8f03e0dd5ffa5ddd", + "note": "random" + }, + { + "n": "0x44c88c465aa6cd0399beecd39d444d386504b4feb83c04a8acad6a81327672377e93e695d13eb924aefacb4bf96385484c198df6c57bf367c33f1bb45f3a7bdedab968397ae642cb9258f649fd84d15389b8861c891a1b796ba36ce8ee4e3098a5909f75368d14fc333231e85badb0873ea3a4e3e5f07f015a7edba559d", + "d": "0x1cd86113c5233d964bdeeba426759c9192f", + "expected": "76031397eeb43d56", + "note": "random" + }, + { + "n": "0x7e220d0ebcbce7a27ac3aef1e85b90f3e6d7951238de40e56db11c95acb8d647402193105c33e3ba9c9fa772014de92be6906f1397890315e968a23247b144aed80802f8f2c0cda7902ed", + "d": "0x74e1f144a630c0e81ce599b91c08cfd9c3cb73601d5994b5d258372376fad45a944c1a10cf9d3f1ca54b7fe7d281b1ff289e037ff9325bf1bf893dd43f89b8ef17d3be62273543dec24ee431d3e220f5d3948ae12dd5939a548accbc1b709b969d862962b06216ac77aa1fd7db0c67d646ef78d7977513f1cb7d8968da7cf877fee121f5b51589b777e70eb385fb85a6ece36e995", + "expected": "1af1442b51bfaacc", + "note": "random" + }, + { + "n": "-0x2ff08607f7df5708a8a8dfd87de11575e601fab50d1459cd508e70f8959d97524ee2b5423007615fbd860f2aab5c6e66ea3f6a11cd54f043c5ea27e8a22a7072221184511bcc365e9c46f71fe8a73bab9dcf4811b028cd3490c596ff939bebcebedd11a08ee605d72d643586d94c28e5423fb", + "d": "0xd92c0761287511295b2a437907a7e8e63428477603d8eaa128f8bf3dcad141a9c9de1f17c7c17b1e75f5096c42d2ba703ad7f54df4ff8a971af77d458fea23f4dd9487f36b14777293f4d677c4fa07ab8c67228980846b3f8a4cb99551175ae220e2df328f", + "expected": "c68c415bfae296dd", + "note": "random" + }, + { + "n": "0x5cad5931eca1e255ba557a599a67d7d1abe877d927d6afe98dd9ba4083bc4453b7e613babfc5610c44cb1b411bcdc670f4e45ea6b21c372bd8b", + "d": "0x416fc6b473f8a209d5bc59c153d3ffc30d196f29573995b", + "expected": "50f6a91e28713d42", + "note": "random" + }, + { + "n": "-0xf47f0431245e24285004d0aa66dd52a5c7a88aad672d62e4c32fb092e4d14ccb25dae2187c8c8ff1c9fcb53f5dd1ca8ada727c78a9b6b6f4d0c1c1", + "d": "0x30a67b8a66f0ce5e9911b26ae3d7ce625bd71a48139f916ecd23de2e5465d7f114244e5bc6945e0ace34bd4fc709ffbb2d376cb7342aa93051fd9a405996eef1878d24797382ed775fe53321ef84c18d727c13ed63bcf9ad2990defbbdd885821f", + "expected": "ad141a3204b18985", + "note": "random" + }, + { + "n": "-0x10d46976ee0cb08abd368846c7c534b07c9fffb03eb42372c8120730f3936c9e7fbfbf559222a0c2c9804f9206c10b5a66457dc06cf175eebb7d0fccc06539f58543efa9c4c97accbb9582ed045a21bfa3b1571ef4e0f1705dc679b2afc1ace0bed1fdfa513c1987552dffcd1ee36c9aa64ca475d1", + "d": "0x7789181cf86530b78bf8560508331ab931c24edd407b3ca08cba57e4bf756ac5790e7c1e28baba40ebe935b95a0cce7f917ac0e3dd2da1fd4a17bbc5df09a0cfa613eee8ec0ef1634f15f1d18b14f865", + "expected": "d242057f2001ea6a", + "note": "random" + }, + { + "n": "0x5d0104b9c26302a907936e13af242a657119884fa148b20ef451ab4e3cd19392d630258e2c0c9cead5ba22e9586ef3fc2fd0afc6429fc31bcbd16abc849c245d1c002a341544bf2305701a0d697f44dad480670fea84eaadfb88d5ea3ccaddad7", + "d": "0x7273dc91a9806b23af7048286fc17b305126871874ea5dfb1f8a8b3040a4c6298dc5d", + "expected": "5eea00ce33e0d1cf", + "note": "random" + }, + { + "n": "0x7a058161e7329e32219aef1c6b00feb6da51e62819d067649774885874aea88b80948a921f76d966cadcb4307283bcc86f1d854a9b29101c893042b5e7c47300769983ef6cf8463753f", + "d": "0x2d5ccfa8a32295ad938ea51cd1d4917a58248e7eddf66c561bae8bd25b649cefd25e20d66e3677e392d24ccd18f339cfc1e6220d7193bcb8a7399f697ce5aa223caf6bb41b7", + "expected": "420584f387beedf3", + "note": "random" + }, + { + "n": "0x3e48708a9d3816ee3360d69e9817b707fd76df1164f1076bb0222029b2e20c223285dbe8e401d7a0f6c13b19f75f2f447a543911a0a87d6b869ef1a989b25b59a0897037384e5ab24dd9fc5d6683934fe9db4687291d7969f6b2616f97fb4aaf277dff976ef9d7", + "d": "0x322864c8ae8924782fe561f7309b9255ff03f29f27e294c5ceaf6e947a98060bc7c0837e909fe3767fe240fcb40ba0a2759f9cb90c73d247173281e42a47e3205f112e5be0f03140a57c7ae01323ff8cc74901e2a482b918272e9e7020bbce941b97058c825b9401", + "expected": "3f73de2b8296116f", + "note": "random" + }, + { + "n": "-0x108eab8f594be0803a82544939ba4271859cf094418dff976f7b2db428cc556e5ee0ead1b2a6bc2bfb4f1c534e89c7cb743b20e606dfa63d553f25b526d789ebc54270d0c4c78551ed102779e22a2a93ebda360e1d4f84b91f9f070b10619662aa3d990433e02c1e0f7ce5b4cce61dea2be9d02f18a1b36dd585c94808369241440b3f29a6b9eab", + "d": "0x21fdd9c8d14393afacb3a15061feb50405f7d21055e87a49ccd948a4b6a4af93c3993f54b383733a1c4cd7f94286656412309066f99461a7f0038c59ab5f66b9c7e7f3d123bd82e339", + "expected": "df1f2ca4d65fff6a", + "note": "random" + }, + { + "n": "0x999774b01d1103008da0c3159ad351e95b8f4e7c249a6139b5b7b3f791db2b0153f76fe3cffb4558fbdb67b7476bdb0f71fa5324fb1", + "d": "0x2b370b3dfbaa57e90ec745", + "expected": "554c6ed9cace121e", + "note": "random" + }, + { + "n": "0x2611450703e02e4f1dbad0d8d65e81c8f21f820ed0429255137427a032b963f7fc12a79b3", + "d": "0x3dd48013442a71faa4dcc642f72c339f4d2bb81c60d96d2e05f35b1ba3fad1556a66b967f3e6141f6c677d05c80c73eb6fe7ee1c1c27e795d7646bee7b57ca128fd1ac6a4f311323b3f6839b3acb35992f7cee2ab5", + "expected": "27a3b3a3b47c2d61", + "note": "random" + }, + { + "n": "0x1a402181c2eb8875beb1c237b4a9ad7b5ed2f2569a0dbfcd91e63b4f0cd17fac3c86c070562044f71d349ce5410b8c768603b96aeca3dc3032d65ba9863f20640fc95d76acd707c3deba3aac1e08872e8e7f69f7393db988e485776dedd400b3d9a079956aed", + "d": "0xd8de830b231c6c7ef49e52f", + "expected": "6cfefcada8e8daee", + "note": "random" + }, + { + "n": "0x2f93984ccca558b782ff54b72e9d1b0706f6e5e163e49a8878598f26026ed4c078cf7a3d023928509bad79f41499841a8608679aea34fa91852a5a210ddaaf666378bd640df04d45bec8350c950e43967df00b3ef70eee57", + "d": "0x330e9f3dc6bec9d4da2d345c5", + "expected": "65add18cb27f4160", + "note": "random" + }, + { + "n": "-0x2057f45690a3d8c7afc37f54a805d", + "d": "0x383383555dde23f764908a988bab0ad0d64f6a70c67470e08b5", + "expected": "ba626a765526aee1", + "note": "random" + }, + { + "n": "-0xb7d787969001917b4559d96acc8c8a438e4354590ffe622062a5fca451d07248573e7cc750c9cb6131acb21d11090fa6ae3eeeccd8e33d8e1cda074e3eefe6f6334a18939b2af10afa475a397dd9097864a1863cefbd2007e9fb9998438ef78e8b32c87a4684dae5184b837bb91526405ad4daa95f82cbbdef9744baed1f9242a41b59d59e97ac3a45d5b", + "d": "0x6275ab043b4c905e1917aa6961133690cb8ed92b8957f64fdf14f3079acd764a52b938aa84c6964550fabad4be64f4d06d51f64182a1bdaec8a9105b529310f5891d16ef8f5764d3dbf739da450300b50bf78c2f759a92dd57b5243f8a504eca8331e691b10c59ae1abe84cfecc0396a4ff8114d825746b4b5", + "expected": "c8bddff89185525a", + "note": "random" + }, + { + "n": "-0x64451bebd5b1bbc17ef38d23fa4e8988fa770722c163477642540f085855e2104ba275fe03c8703122ec16d5d33d1ca89a333d2bdcb91c5db6ddd66977d583ca2f45c86b8618e3", + "d": "0x174004a0748a3e15793b80deecf79908661b69ee44f75e0d7381a71eac5c9ea9c804e91f3c8563a90ea60618406f02cba4ed7f403c68fd532adc146075f1b5ae42abe1db489b3db647284eca890ab7299e286fe24734a792261dc76773a05655eb7afbbb374a994ece2f0073defe340e1a3a535ab4f", + "expected": "a8d1402d69ebccb8", + "note": "random" + }, + { + "n": "-0xc21010d4c327e320b77acc32ddd79c8795a469a1e7d1c25a41253a6fd8130015bb3fe27a50013f8f4302eb5bc0b7d7eb7d55c81b454f95cceef415187fd36f44ede2890fddbff2e6c7943efe49c7416a430b59d30dd9593190d4106d4a589dec18c4781ecd2580444e7c79c3135de2549449c61450e9fc5e434da8e1d3f410e0571", + "d": "0xaa0df843e23e503f668ff1e803ac3b8d82bce3960bc2da123a4d87f70ddeedbdd5fdec3a058e9bb938d70c96055df2cde534aa14ff544092c4955dd7a986b9edac94fedbd47321", + "expected": "dd3242453d59a34b", + "note": "random" + }, + { + "n": "0x6d6e28945e5a70b32808fe36b17aa153f28f2eb9103dab9034f49888bc6f275ba9da53e8e8a6d44935058d2681008fe92fca9a7e0c59e56e15f92fb8fa1589174eb7c620e7cdf4bb0f75f4dc161df1521ae52f63a5cf6d9d0eeeec4095e3fd2a84afb6d26ca7f5a9e862fee5ac5a2452acb", + "d": "0xc60624da02729c46804d63b697acb6e8436bbe18a72e6c5ff5dae5cd296d79fa82eb94ece18f1076be068e2f5a39afca093ad3d02c730ce0fbf01b1", + "expected": "5ae1aefdf6daa563", + "note": "random" + }, + { + "n": "0x10b22cc163d3159418bd9206222023b3921db52f5fabb4d16a1115a4dd6ab59019326edcca045593d7675a297bc9965060e2fafc1ff13455ccfc6f8d52849ebcd2d3b18371827deeca1e0d50d3f9d70b7", + "d": "0x5e3909b6f32252943bc6a2f0bafb8edab66503c8c972d7716002d87d303f3", + "expected": "58c6ae63ea81ce65", + "note": "random" + }, + { + "n": "0x2b621fa386dd4d296733a57be37d16520a19dabc54603f0070d1cbf5be0a53031a583f2338ca32d2b289305d260336aa21cb104243ff598ecfb6d6080ecf6cd77fd88ae37b74311fedeb41", + "d": "0x3171b18ae4010ffd5f9abe5739c504dcda87d1ac26d3880e0fae3203b88d3c480cf07f36cb0674a315e2cc6b0c8b8ddb0f8e267f0951456b1cd9127cf50f161fb55d785c150e8b567e25b3b50937b69d5fa71dfa1a784326089a01cae45a300bf37be892a0f1e28a4cd739a778f682aef13ddbd523a73e7dcaffe518413f443f86922f9fb96cf4092710716b76937bc3", + "expected": "1d6c13d4c98314db", + "note": "random" + }, + { + "n": "0x18c8a8a3160314d68fc4b40200f7dade1d2da222f5881a9e0f10d485426af18ec438d8e6e56ee1bbe0499d5d34143cfdfc7b1deac1706a7827c45dd64df52fdb9443eafe7e7eefb33d5e678d8c8376a904d6b73c9a0509bae548c22f6529c281d1d933f8735018acdbdd2fbaa24d9d4f2353", + "d": "0x12e077e24f4dcc2677a6b6f7223e667bca819f227874e633bbbe872451678a4f0232d8cb241654395f4ca5e6490574e3951a92a3fa05bf67cf9ea3600d427a5835d970401485279a6bfde5d9ad9cefe50498e30c26e2120aadd50622591276fab9b0cc386390a733cead6ab3638e121a13cf8361da81507d1823a08bf68daf3b76c43b38c7663649a94b133028405ce5de1416b", + "expected": "2f3501bb8a3bd6b1", + "note": "random" + }, + { + "n": "0x1b27f2a6e578da5921ea8e9647cd69a91d60aa0d2b6aa1f", + "d": "0x7a676e336ab2aa7842d6a2280eb76fc98f805b561d55d2325b3ad038b69cad7f1e65e8f03dd8d4f969645be415947e45f1820bd57ca6a42f6537e117238b4fee2babb5e30d45bd37046073d0b6e2444efe493b551be3475c83dfce480aa0d56788ff96b315245cd024c90a28e279de39ccd3ffec7862c1fb7fffc93ab43a7c24b4e7ab69c2587ffe1ebee7", + "expected": "060c65c591e2b2ce", + "note": "random" + }, + { + "n": "0x5037c05a6c6793d1b49b979ebbb950eafd37730efcf17ee052abadac6ad3f9e21d97543b4912c87ba69bdf56fa20a483ea6a91", + "d": "0x2f63007fea4213eced", + "expected": "54fb15d74616115e", + "note": "random" + }, + { + "n": "-0x11ff81", + "d": "0x23718f", + "expected": "bfe03fda6d1bfdc8", + "note": "random" + }, + { + "n": "-0x5812b1f596bdf42553fe74e1a24b0d05c83c2c9da882fafe9457a30a376a4b7a5b25da3cf394fafb7f131aa25505bcbde2128be6b75e8162650c2654266c970ef0eb812783d3cf8c4902034bb73", + "d": "0x10cd607b84aad1c8feba34aa326616a614f8e19c42b0adf8d488189fb9c46f79cfa775dda79df6e5d6f3cad0b8b0d565d05a63e72abac2c0cbe7ec534ec72fa6422c51a27de299ab3ec7cbe32cba6dbaddd", + "expected": "be14f78a7088665b", + "note": "random" + }, + { + "n": "-0x6312fc4e558276a8e1c813ab9458bc4b15a45569cacaa98c8a9f", + "d": "0x32fec54b22b8ae236a44af5fdeab8ef82b9860b05c5f3e440f4d79109dd1eb4a35706a061541f0746d4206449a4d0d681bd14b9e2b21fd3bc94344a281393859f703f2faa06add4e0d11d97817b0009f273b1c52d3e0755872672263b58d0d68e084780a7d338a2446d126ca295", + "expected": "963f15c3ad7113b5", + "note": "random" + }, + { + "n": "-0x16ace2260e924d6b6b62876e30ac07db26d8425da566277bef6b9d0e7e8fb0a97e3e85198ac0f9b0c8a86f7", + "d": "0x137be2f998b4394c7d4349f7b6857cbf2faa805706a28fff1586f81df127b088f7af8110f935e16d45191cfc7e614f9dcb1eaeea3810e2bfcd2007a6297fd1a41bf4ceaa8b09299cdeb7", + "expected": "b0b29ee8be209ec7", + "note": "random" + }, + { + "n": "0x6505f10b44284d770c41651fe07262c98889d79c314113e9664382cd075af2dde44a552d80b5af2747efdb97c17b39b86444f616c545c9dbef0b31986c17445a05aa488f196079e4df5a16c8558d86312a6f90e2b7c1dd2998bba044918a2b03fdc2d079c339a650b81e1e96010cd3bd31e2a97e7d7fbd845", + "d": "0x59745e98c3a50534c9ddd099903165b290f943e4d6ea952823ab184642b97d9646a4e01b87c39c9e3ca6178af8af3faee8c6b7aba918f6425e230303dc89aa7f7a1577f3f05d8d81663c7b54bae01362d46e56799a9307c559e98f4720ca0315d04c772735e8ee8f6a4796716052daeeadb5179ee2dcea899662306fefdfaa6e9222ea3f8c6d0a1", + "expected": "387211b641ad4f27", + "note": "random" + }, + { + "n": "0x42f2e5d0d508a65856fa739b9583cb34c2b29dcea0476b7d4c1e04d0551700e25ddde27acda105c5f8d670edf4e3fac5fbf2ccfa5da08010abe071dab73c2c036916106ab41da8a6d88e745f5260970e73f31776c8e60941eceb0fb", + "d": "0x1412f405b1e7151d648f63bdde3279d5cef34aea9e2f808755c17af05fe8e9ffb3a389c57437011e3adbc2a95eacaea0bd1e4b3046539a1", + "expected": "520aae465812da34", + "note": "random" + }, + { + "n": "-0x64534e9a34c284b676359b8df67427a82fb30dc5df99479792eb156a5a6ba48aaa3995c3c99583dc72b70c62e3aaecb1b88220182a621c4eb36b04c2b4cb3cb819159ca8862b36a1e3ac6ef3bd827aee9ca2a3f3b5af9b12cc257e5c6cd1e2ffe6ad5", + "d": "0x7fa0963ffca500919734a60e4fb7574958d29dc46ed44747278e13d18ab192d95a848f615f16023357a945f0f7f051e625a7e0a9d188deda17a340b62ee1094b9ddf4928b6a6db68e14b0026cd56543a55ff36584ce6dd9979215c6cc5ae784a0c08acda93ef14eacda69", + "expected": "bbe92793ce73b05e", + "note": "random" + }, + { + "n": "0x41326e68d1b179f96c8864e7ea77c569e3f3a41f53e06382ed0c43c2fdcddffb4b43f644915c1a328a88538457ab21c5792adb6bc7c43ea4dc7662bd551b73124f5d57d2ba2251e6d0ed689c19dbf40b79449c28dd8efffa6856033b0e0cbfbd34ba71857d99852afa2ed4b4e9d1", + "d": "0x7d9e4a5c4f28f9a2826ac05db15aa2b350137cd694ef0a7e1b8cd0be4ff3dd492ecf05235303f9286c95f6acb2a768424f138cccdb", + "expected": "5c609bb80d535411", + "note": "random" + }, + { + "n": "0x30c8ae75a2a4c85f22a3bb8ae14d281e0b468ddb576cade9455f0581e976d40d955af51c1834fbdea2077f5c65b6dde92621fec9593cac57995195c3fc8ae08240bb5eb29dc728963452f9c6b92b18ed7521247004bb9efbec9f587c20176fbaf99415c996f245b24542d2ac7366803908c854a173512feab76590b80a0ce2c6d", + "d": "0x46868f3d917e4552b4c06eb", + "expected": "7a66228fd6fb04f0", + "note": "random" + }, + { + "n": "-0x5beaec1197e4a531e7f563a1c2120e8ac49ae076937ab9add5716fa898b36eada3ca8d81e00557c97e84c5a39c0e3b9a5c44f7e3a1935968fbca863f86c975159b7046e4b94810532dc6a8b3dac1bb7386e6ee2780b9c43eb10a108d8ee83cf6a5fec9a81e7042f72c9da6e23", + "d": "0x9fd73c1c93c9c4962c2a1c2fc2e2f071a29e3f56039289306b88da391ba882da2c2fd95528a844b6f91ec8365794945a4de05f8e10cf9abacb2965ca4d020309075863dbae18de989aafe68a75e189", + "expected": "cea266df761dd90f", + "note": "random" + }, + { + "n": "-0x23aaebfa9bb28cba68d6b848a22dbd804777a258715296b2aeb395eec8d67af1e1e0fba9dcd86956eb6d845385c98bb433e6f8cc0bad", + "d": "0x3458a5a6bec407c7fdf3ae5552d51fa5e439e2cb974ec4b40d0433ee85552ba96acdbeb82659e190453c1c0619de310d12aec6d668abf787e382e5ef005af05e978795bdf03f449078dab848e966643a8a164752403d3c4ea0f2f11d1e59ae05937b7ceca40efd94cad9beb16320f2b0c5042536b31f71b5bd4a46b113ad65c5f806e0bb515c38ae4875d58a8bbae253", + "expected": "92e5cddbb0da46b4", + "note": "random" + }, + { + "n": "0x1e7c07930561eb9782f4413b51a1b743c6ab74673cebf86a9b7b988af64f1d2eeec843cd7", + "d": "0x15ce07c2ff33e7aaac8a4adc05f7e1d924381ed0e277bf8bf3cdff61bbe009cac417161a8d2e2b56ed6517cf3af84c9", + "expected": "3a765e773876e6a2", + "note": "random" + }, + { + "n": "-0xde9834d438d9b23da446a196fbba1207bb17169797d0d75a4ef7044027b26d68a449dd54c96e34dd576bb07632fb0ffce67793e866468f5fba83750a434499f4e7371b859349679812da9", + "d": "0x654beaadb7dc10d3c54116f3a1bbc05695366f12116272c323c16d65bc04a736c1c6804cfda8c28c56dbde57d6067d756561c31ceb70e3fc236de73a41c0f99fcc2b3d38560917700e2cdb7566b6508440b3e317265cdb1b472ab9aa681e4a7bb9b5fda907e27bd388c515888e847f48f3d379d89cfb6e276b25de69bf413da82a4de63ab3bd5c3321e56de7bd76b891719cacd1", + "expected": "9b41946302b05f40", + "note": "random" + }, + { + "n": "0x2f831f7c84f54cdd4730f7c88525e0e218e7c02911f56a67d226a2e09b9f2a87aabf1d7ef", + "d": "0x1de783713b7559fcd21a60d85720da1b87efc102ffd47f24b5e667eaf", + "expected": "43f96bbf78520f21", + "note": "random" + }, + { + "n": "0x5b5d26ba54ec39e4d7f64ef1ad655d5624d003b896159f145b1ca9e21bde262e82c7021680235603d1cad3df578864de4c1cf2e5ba0493f849", + "d": "0x6cc086d73127957d7269a4083d494a1bc16448507ca270d04907801e59e73ae2d4996171e2f9e5553abe4a0634f73c6a467fccbb5339f2fc4e7750c73b20455321a96571d19a29deb283b2f14a86325072c6fb5b7d7843ccd51898d2f17218a526131f157f490e7b20e51c575851c58e545c45df86b", + "expected": "21aae231dc34b2c2", + "note": "random" + }, + { + "n": "-0x314a6dc99496b821e1a0bfe825bdeab5850cd19b6c57d361aaafe199700d63ac2397a798e0a513c3487d2a162180132eee7e26b9f4ccc88b33500d55033ebae994afe20e40a9bc67dbf795cddbd8483ab1dcb82b2204d2c000453fc5b", + "d": "0x1888e0e0f3904ba31cdd3fb2d68a35d526205a12eb1f4b3c40bf4a541edff6b6cb5ab47e04aed45d23d2bde9b09c3b8cd36091129bec54fb0ed6bfb19b8249be95ca991e1eebbd9432b9a15848335cc8f46771ea5933fe609da678797cbf1ad30f6ce80bb8679c582f1a60e3e6a51df72fa307b39853317f57c2fb716bd8c4c77c757dbe35da9813642cd1c99650b", + "expected": "a700127a9e919cf2", + "note": "random" + }, + { + "n": "0x85988b504d04920c18380fed3cebc7ed04c913a3c4c8b41268fff0e0583772cb32aff37fa1d7f0322d12e96184a7739fa17539b902b5ad33432e6f173b6335e7508712779138eb6400ebe473709", + "d": "0x28740cb1bc487aa46db50b63d65a099", + "expected": "5f0a6b7572a1799d", + "note": "random" + }, + { + "n": "0xb10b13ea34f7e95d6ff509aa271310481ce38d06b082039fd679ffd1d8c36bde3680c871268e71c7bf23a9bf142b9a4a9fb042566b2852a39015f6cbcaa0b020aeda9a2da873114dd6098174672372f262dfb2fbc55cc7d5c0e10d736de66fbf3bc726c27b235c66cfc89f03daa854be5b1d6c67a7faf8262919accfa9d7d499d95d8cd3c8aaf", + "d": "0x2ffa744e20faa8077f4e16ddd0e5ab3febba4facfd1c3a07670353a70168f24ea9baa1b407cb30f7e2e5c0cf35216629117a9c51db8d07e0348a78bc923eee6fb3d4f6a0baed58789409ab5995507e499c37effd1c48deb282ed127f70361c908b5deab2351207e63a0983ca42598b0694920a8e6c6d684ff7b9", + "expected": "464d8541c7d6033f", + "note": "random" + }, + { + "n": "0x14869ea93e35bc29aa9d9e3517ccc0935f2ab110af27ee8431479ed3ddbabb33f873c321d5112d77c7132cd2ce1ed55216272f8da1d0b6ee3139fbcab5647dd6edcd1eae5fdd026f1bf8dde47e00316918502b1f590069798b775ab23c5fe2d8bfcf3d69c39ddfb38f8d58f9b260c29e6422ac1235d4cd72c0a830624124844a44e577324f4afdce3aec5236a127796f756d81", + "d": "0x6a0bf3306048eb25e40321e944195820622f712ec5c24c86d84636a70dc053ee5c6e41bf821e9a7d7d21e8ac3014af99113cd6582e76d0e7623cf6f2e2b06f50291e906ed77b83bfbc6734eaac6090470576bf1e0c344d3fb6baa92205d736c672d7718965947681f396f158e72bc8ca20ec92d", + "expected": "4f88c668804828ed", + "note": "random" + }, + { + "n": "0x1adeac3560b265b3787190f243c44fe5721acd0bf166df4837a30b186f10337092deedd871c95f", + "d": "0x25a4ffe44d227b480c2034797c83efd61e6ebd86a8926dba5e6dc509bc7bd3ae5b8ee70bdb61360430d3abc15265d6c8f5e7d6d15cee4e7227e9a48efeaac95e4f292175607616aaf431eec1c6ee0df56caa19d02e5ba862102a865b6b215e09c88a89df9e669e1", + "expected": "1fa6d743be23f709", + "note": "random" + }, + { + "n": "-0x181f68845399411b6bd6721fac7174fc6fe24c6eb08fca07e7825851c3538587ea49b063ad", + "d": "0x2cd6f2a0126fed750aca40114e23c1f78d9e0d", + "expected": "c8e1371b3f742c62", + "note": "random" + }, + { + "n": "0x1d20fad456e3de78ceb74653e91769ab4d4040f1011080996766c7fc2d51363be12b312d041ab671cd14294424b17d08702aa57ac851d51c1f0eb4070daa908bb37551276bdaf963286ea98092c054b1d0d", + "d": "0x48ab774db22619770a7a3e23b803b26ba3458f88cc4bb5f6cbc0b0b682e0b8b07", + "expected": "5859a7554202e577", + "note": "random" + }, + { + "n": "0x2ef68787eeb6a09a4cb78a47ebbf4e928ee3b1da7959111efd29689930352442e19b1f681e4d40dc8023340ec29e920ba84297f31643dc2a7a01754c40224e7101c4b8108ebff75ff90c382ab30b7060b3f341cc749fc8c7bdbace0633016cf482f6bafc9d282c1335de9582d2ec3d6cf15f3d2dbb142f7b", + "d": "0x1859319de36f1ce0f2ad29b3605115b251cc476ad89567dfd70772a5e10051636d6da8c1b31927364b3c19ee89f0e347fe4cefc0f62a80068ed49277e8509a99496de9af0463c77ba8e602a80ab5", + "expected": "54fedc542af8d877", + "note": "random" + }, + { + "n": "-0x1742cc61f6b3f57e41cac9d22dfed3589b402eac9b", + "d": "0xdb34662028c70d39116855b396dbb479f33df39d167320fa251e34df718710b9b0028db4d1d9a3005fb60f7c5b008da398da7e53eb01b82490ca6b47d3cc2a7507198f879bfc0df8aa5952c6eefdbfa7b2bbd", + "expected": "a0fb2a5c47391960", + "note": "random" + }, + { + "n": "0xaa3ee1415eddc9ae66569", + "d": "0x2f7928914dcc307792965bf412ef87969b19010201283ed1edb6ff2ce79ad976c20008d4ac593a6becfdc8f9ac9827c44f", + "expected": "2cccb0681b2cc205", + "note": "random" + }, + { + "n": "-0x2d09e6c73f5da504d60a0d8822a47d624326126f844cd4d1cb4852cc54db1d9f50fa86cd5f232bf02e71c8e121f1bcaed3ef999168f31adad205166ad53ea64c997b342af2ec5cd7383", + "d": "0x69fc40da84bee6613c8192d976dbe216f3c2cdb4445370f81b531516d3764902474bfec8d3fc2414819a207e2f4b18c7120f357bb6593204eac0005daad82ba63748920d3bb1c7832892d990950ccc0d4dd437a7b9c6e83c7138933816d3620540d62a2d", + "expected": "b29b32694794caa9", + "note": "random" + }, + { + "n": "0x146482bbe6f8aabe00da2dc08a4492d9562713de92fde4730fee424acdba92458a8db205c528dcaeb4cd5f3ab222b65c7850e008ece57087a0b57da34ebf59a00e235986a18c0f387a2dc0ad0c87ceeeaa47dbd2163fab5c1b3bb469b5a49f443197b69dbdf131f8d5de55852f8035505885378774417e169089eb2f", + "d": "0x48d3e4c2c6464fe6705bf473a1d5ab11ab491495d80a2847b1e7202dfa2f8560650cdba7a32180f18a5434b8fdd36572c709cb25a9d8d8342365d00d7b39005d81c469ee8ef167387b88aee86f385771a01464398058a7e6da6d7f", + "expected": "5051ebb6b8342fa3", + "note": "random" + }, + { + "n": "-0xbe506ffb5a71a77eb4ceb7c5640128c2fa9032a576c27beefc17ef90c6ca8c16da92cb1302376c0c63f75f33eb7040185fd62aedeb2a1f531126179a1210d86dec4ad4f64842779dbb767213508c07d994b0cbd17c871884af0e7eadd476a078edce8cdc6e8630b0ce8b4b75b1c013cd3b46bf50a77a9f1beca10ee0c1921aa8cb99b45388c1b6024df", + "d": "0xc0003ccdac4735484be6f701e6d4011f1f47b87b95020eafe15a89cb82bb3958764a130b6a3a718e1303641a8463008f83c6a04f665e11efa6542aec4c94f1535a74821ef8ad18aa3496c6d4975429f7fc4aab28d0d061f17e6be3d8f66df0034f4229d2b45b9e74ed3de229fd58fd69dc9f67e03c03c8e2b34e25b3e94c195f2e5c1c19a2899b88c3", + "expected": "c02fb8089e676cf2", + "note": "random" + }, + { + "n": "0xd2ff925a7f55762825bb148dfb7257c98aa745ac38ab53ddcf35d45f4f2666b92b100caca8334aa9b5ea2ca378e4f94950a49525", + "d": "0x14df17b6979ab842c49398d92cc92cdb0ac42604af72e972ca95dbab4b5c1c38cd2054b41dc93a9d8c7396ce518dd0b37101bd9f9de63b3514732d642e9aecdddcd295", + "expected": "38a43806005199ed", + "note": "random" + }, + { + "n": "0xa7e8025c5af76245f9a4143f0c582e3db81016a3445b42d902bc7093d1cdced66a74a8d160a8a99a4c565640a423957cc7c61c19eaad4eb6cbc6b4b89a2bef", + "d": "0x36e1f9b90e2a796174a242115f6fd405cd53ebf317be91f6466b2a28d996348f5a87f838bbe37ca520ba9243feaeb9049cbcac599f73c3435800f6b42c3a9e6433c3655319f82be8d06df7ea6a6d0135c87a9cb8a2789d0a00b9da5f9eb4aaa867996699e513fc7eca6d354271bd65558feca5a77921db2d2ac3d930af23e405db9d05daf69aae6aa6e98bffcb86e73ab3755", + "expected": "16487994949ec04f", + "note": "random" + }, + { + "n": "0x2af91c0f3c0e1ed62800d84518bd4bcc300feb10321c70f6fbd30038ae706a1fe1ff507930f5f916a41", + "d": "0x7decd57ba11abd099bf10f5fe27b5bb3ea2fa65d4fb7d250cb0a1a56df3bac1d3308c98f0563e31827bf5f315c7222daf15cd0d68f8d4f7304122a3475e5cf292f9db26a671bd63ab1508456271792a8376877c0adf6a626d49d7f04bed85aaacb1bb28541ab88fec6e346df33bcf3793cef2477076782b5eb2ec1ba4061cf79c25a6cf758698c3485412c290c0346006f9c4f", + "expected": "0b15d72ff9d64eb4", + "note": "random" + }, + { + "n": "-0x3789d9def37a46d33", + "d": "0x93f0c51958134220a98a9ff884a6423427a35fcb1b4f71f536034d5f15b161f545943962852384a9cd631a1dae90fd6ea70c4cf5ceed7071f7a756be124ffe6e6338394f9ecf8207759d70cb074fe1f44b31720ff27d07acfe8651440768de8b86ec0d3", + "expected": "925806bb7c5c70ce", + "note": "random" + }, + { + "n": "0x7f2f89b952523aa6ef8b890a2950716d2fef560586bc127374091d7bb3ca1511a02db79aa3f95939f35cdd5ea34116eab33fa51c99c59d7ceed96ea9cf87e0faa0bbf4c46c9deb0c9a360e2a9fdae47d5f7856a4695b71d37cb63afa0016eed1e337c6f71c8ea4c6eb05867f4a4ce282356ba9933a600775b52a2aae0f86c06551323647af259193e909", + "d": "0x140d4fc468b3872fb5de206f9c42f6afacf62f8a7d25d8be28a24400d6135a181c26049c8d8f8ff7c72b2dcac004bf15d5d7580d16eb55eee55149493cf182fec5af1ded09920c115a507653d", + "expected": "5ed95f0564452a55", + "note": "random" + }, + { + "n": "0x47af0601533d3c334caf8f3dc188777c57bf956a71522e34f7c6a4ed1d5cf1fee41734b3744ef77e7a15fc3e56e4f7f07fb8b95bbf71d436b4b521f403d65f44e0e41593ca720d0a9a8f3bfa3d59d", + "d": "0xe2a05d61aeebd5d925bdf8a887fd9c33e5ce6f3d6313b9c3a8277a7e039b2b2aa0d1ec69ce8b64df2f29b1c89", + "expected": "50d43e62b100f926", + "note": "random" + }, + { + "n": "0x819e1b6334ea0a1f055c229", + "d": "0x6a22ccba58b54be29157a669304bf79f8efb17028d75a5beec86b0f8b2a8b11cd6979b9ebae3626f4281569f7140718d4bcb4b936ead2d1ce3fe4dd8a17da503687f101874ef4f91264df0455", + "expected": "1f738a347ade2602", + "note": "random" + }, + { + "n": "0xda3", + "d": "0x303cdb56cd273a1f31307bd6e8d2e243fcb2b48f6d27aec94c8062484a4e8273ce9b1555af9d", + "expected": "2dd217ba54c6b82d", + "note": "random" + }, + { + "n": "0x29037479520729603b6c56e4017ecd21ee24e0abc2348b003ec9dfeb4e6d882324ab8798ba786b620c153e279989cea7f7bed9b5c031abdbd930e311fef15c55e226b7a7", + "d": "0x17ba96d81d3b03018e437d5f3428441b0a54228851c82a95779cdd6147da671c877b05cf898bbb6f407a2f17413c28143781b1f4b1a2f527e0952fdef06e0e05f56bee12dbdebab894f7aca5eb76d5b8f146fcede1acf9b55d9d0af4d05416692f0e30228a8afd64953a404ca75b5e6c8360a0035abb52ac512573a3", + "expected": "23fba79e0d3b371f", + "note": "random" + }, + { + "n": "0x7cb7dd69b7d9cc1590886eb2851aea41dd1c51e1364d7834420327256f9ce11884e3526d8e894b3156fc6c67f671bc11eb424dd672a4ec7920eb4e445102133499454db866494dbd0e19a86562fe5af43669e40c76a0fdaab149bad9b0c6c4844d9d091853c05b2c8d7a4c926e9332d0885a244a5ef4712ade90c2806a10c883af2102f84f05787ee12d64ad587", + "d": "0xc9999075e6d704e339abf4778d9dca51a7ecd998003295ffe904ecb758c6302d9e0bb8550d008182479f9cc83bb6f5ac84a2264ad1a596dd51586fdbcebfa2e1865074330825594971b81f974a98b040b20910e42072f556b7888318b8d44209", + "expected": "56a3cbeb274b5df5", + "note": "random" + }, + { + "n": "0x3a9be456c47733c050e544405d61520fdec88af24e71e8c404dba004fc66e90e065d6e1a0543a629472c2481", + "d": "0x1be3ec46392fd4c87ffd21bec1d264da66d31fef595fb0b1154858a541402ef83cf8a7e70b196df75db04c16e33b9caf438e4f0ef795894dc6ceea31b32d7f59bd8d56755a5d7c359475a35ca109ad762e1ca3573555363e33f3d606b173a949c65eda66549dd033ee5ffd782fa6bf244301a1ee9fc34b1f1c2f7980025c0ad8749", + "expected": "1540cfaf0d748279", + "note": "random" + }, + { + "n": "0xed3b1df988e06d8383b2a33afd59335e86c1e292d08249239c8580e2e5edd16d39c5ad5debd3919311549dd9cdf6856931589625", + "d": "0x2e2141f7dde4c1736b64f160647f5017451fd83bd72dcf6582fc25740d2767474009ac3a3a0fcf24c3cc34370dc6f36682250df6031bed364d23050454e23d1978d5ace72a15950c8735e35ead7623967ef78e4af7a8c5c13c2b8fe58738f", + "expected": "2ad49218202766de", + "note": "random" + }, + { + "n": "0x64175a455fc05e40c1f019c305183c9b3c18c2bc255fe0f9aea553a71", + "d": "0xab1879ec7f5b2dbcd524cbf947030517ed6a55fe6b6e683b8a4789cf", + "expected": "4022b85576a07f4b", + "note": "random" + }, + { + "n": "0x3355cd834d2cd04d140dc7e450ace6604625c4b86fdd00a596c8b1d96738220a882d2fd58f359bca5ec590f06d6b9ff934caa00274b644fb7dab1fa4cb014074a4473a861e9dcc6253d318454906ac78d36f41451d80ed9df91d8abc2036259febeb5996b2556dfb9fab008f2c2cd5", + "d": "0x4bb5383577a4f96a3f8ceef0427f76f1888344862b5ae206bd5c401d5ce79963d050f338df7d66c3b058adf0e7e087228ffa7cb24d31511a192cc510d51b24511690cd47b0b1a9dfc7d12d4b", + "expected": "5165b2bd9c0ffc08", + "note": "random" + }, + { + "n": "0xeed133009", + "d": "0x21245f5d10442c06d0af9b69a055cbccd491e8f311232c9378455aac3e4568e5b067a9cc0ef8523cc2e3fbf95733a997d7078dedfef9db049793be4e2d5cb25f3", + "expected": "221cd2ccec804510", + "note": "random" + }, + { + "n": "0x1bbeb48776f5bdf2d184b32b774370660b617f98aa42a7f86ce92ac9ce2337d965c22ab0a27d1442d49995d9", + "d": "0x3817ce440890fde6f8dc1b3fdb7dea5aef62b557615", + "expected": "4b1fa7eb9c2b376e", + "note": "random" + }, + { + "n": "0x3b2d7becb953b12418dc5a4c9748250bb7850c2dd378c012eb04590eead22dd2ddebe536ae08a9e5a8c80196a8d07", + "d": "0x15cab963dddefc7d35d1f92579fc03095a347302ef884696686bd997eb5d7287599d5ab868542cca2e4d5235fdb59f145fc81d4696cc7d7e8a40d69ec2fc1dd9e82817886698fc86e2c9b2326a1eb16b77d52b46346c21", + "expected": "2bc5b983290cc40f", + "note": "random" + }, + { + "n": "0x19e8b1ee417bc08bd0420bdbb73e5156136428f", + "d": "0xeb57c5bd61b23", + "expected": "463c2ee0a8d55caf", + "note": "random" + }, + { + "n": "0x6629573a3b0fde52b19bf907c19182395e8a5990d9998336fc0a215a22ec158369cd56652a96aabb0d575cfd43f36550a39e4a4f157203c50ca453ea0eae112632e56a0921d86e344e30c761ea5e8026c062405615c7cb", + "d": "0x10ff323e33327e8134e7beb3c1a52387c8c0c452ac0a2f3", + "expected": "5fd80add2a521b9a", + "note": "random" + }, + { + "n": "0x71d8b495baca4ed7eef2d6fcd474a57b1c1058d616ac4188624bcbb8f14a1e45a6beb7640683980d329a11a0a45", + "d": "0x55718c5ec3d75662203fa4e2eaa9f96af1a96ac8d358f9a5fa8a500b87aa6c61ac58fb30f65973c1f52eaeb17bb2e811ab0e63b2c07afd5862189a957a4bd9102492f136eb6e2545f02133125bfd455c719e72358f28534b1989c4b1ed29d722625b8ca93571c8dc8b67ca785622ec90e25d8b", + "expected": "1d3551955721f73c", + "note": "random" + }, + { + "n": "0x34959de4765388fdc8771b4637547caa0e3fb79657bf4a985cfa7", + "d": "0x346668775ab9f2ed6fb6be6cd37e71bdfa2d530523a1e8a3237387ae613b7113f86548001b87a397147641af50fe59b", + "expected": "35700e6a3449f829", + "note": "random" + }, + { + "n": "0x90bfeb5c8ec748798ec56f2a041db763d26944231eaca279771733593e6cccfba67c61d2a80af1edd96cb5440be93cc1e8a29f79ed0395973c16e34f", + "d": "0x8be77be7b50a3ea56537cf4802b79e66dd41ffbda9e92f7c90e0b2f7c5546ede4d2741", + "expected": "4c708ddc6fda003d", + "note": "random" + }, + { + "n": "0x2ced4bae0590d65d244abb89d6a0e91c7187c34d6bcad7ee394252992f20f7f54b77f659218cf6022d50d3a45269326731", + "d": "0x1ff2be010c04ec4097a7eb24842ebf6f39db01f157debbb39d7aa4010581394fa5f45a097b867c323d9847558e442960e388338dead748b6b5ced0d2bce68c55", + "expected": "38767ff83b0e4cd3", + "note": "random" + }, + { + "n": "0xbf75e11b247c40ec56f717fd3aa8871bdaefb440d2fcf3c582fc339a37ee045eb567b7c8af5aa14f33f237eec12499449a7e0f80c58275d7426dfd2b1ba509b9f50ef693c555ededb", + "d": "0x81d12f277eaf907442f9384f8aef61412a721e9eb0d94133a129853c839764c517ed65ba9ff0cd5bf50f2b43850628b92392e1ca29c0ed2ab6eea346ff2ccfee126c9c89c2ce53f865c1277bc8f6291b79cd9e784dfb16108a56bc443e31aaf2c943a542b46ab3249d26f3907203ee12cc6f3c847f4436eaabd2377dae2e5f059", + "expected": "23f798f9b6c6e8c8", + "note": "random" + }, + { + "n": "-0x64e897a98cd5fb7d5e4a3aad39ef6fb8f049e56aca4af66abb5cbb789b39750bb14a8cbf78bb2f09ab76a58b4f39125cef3346c4c18aeabd6e7917b1e316b4e65dea347716ce9166227e866f3397777f19abc338a6f93b6ca39106abb9114261a3b9e83a6f7fb897e277dca4249696354163c01fd36f8ff351a5672a203177b27c51b55f677c3328f0cc962e7452341a3e7", + "d": "0x1fa136b8df873ac6d74bd63d29023a2e7286c5580822403d2caeeda0b5409e557de2b25c5c66c86483ce77fb36ef09ce853d0e3856bb026a4a4cbd10052ed735f456ecb9b5bb50a229863cdd74a890ee0e49362e67f", + "expected": "de0985bf55421dd5", + "note": "random" + }, + { + "n": "0x8f0af353733911ebaaa54e670b07049d4654c20d2c664b0cce612cebe15bb8d49d8ae42344b371fe7a5389d0f354c71f0cfea94db7ab7c20bdd7f4d2d83e33058c3cc51726c7a7", + "d": "0x572b631a01dfd298e04d9c81d32ac0f31d7930bc4b5c79a3bb11b42f2edc06b", + "expected": "53ba416d08a11a25", + "note": "random" + }, + { + "n": "-0x5c82ed91792020b8a1f4fe57a63453354d98c34ffa854a359e355635fdc463339a5d859", + "d": "0x1311ef3ff47e5f41f1c6b7a025978fe4e7b1714a725031b2de94f93eb82fbb5329e429c794c6a234824bdb147bc75c85eebf94c12cff221ebaa6af3d00f9feb9d86bcae738a40db96322ed89a1c980da31866256552fd8ceeed245a173c95a84c16d21f8c422798e9", + "expected": "9d9367902aa7b99b", + "note": "random" + }, + { + "n": "0x378e495e0915b304fc1b8f0ce27b4ca0097a30e5fe2c7882ec1a8e376d86e5821aec435431f6cd4182458522b6f38d4f40f10b4ed700819bead8f00990db2c2f027db4362d651996292f404d201631ee3fc5c9c0233f4ca741ab64befdc61e53c4eba8411ba278f05aac13fefacc0237fafa0db36e866f6acc8b61419eb7c226026fa89bfdb55f", + "d": "0x5001b2a70ec5134a4a433ee5c5653b29dfdae93ef7ebf65d0018ac0be0325774d05ada7f2755f390bc01", + "expected": "6e6638716b68ce34", + "note": "random" + }, + { + "n": "0x1c7cb06aca38388b635807328d31338c2cc8aaf7", + "d": "0x8fbd604018b07d46fe502e657a18f6e958a6f7e5fc96a4ed7500f5f67f16fd775b922c17b63fd44c3ffd46ec53cede267b8586f4820d08bc5016f20b99baf154dae32f2dc3ca7e9091898d91dad142a7b170fb8ce6155f08655cd4dfac29605e4b73c966042ad706f5bb1584abe9658dab00f87dbe31", + "expected": "0ec95e208b2262a4", + "note": "random" + }, + { + "n": "0x2f38e1ddb1e0ae9bb9934549c95864bc793a56a43fb6c7a75643e3217168041b0b6e0e4ec8fb69f", + "d": "0xd28509fd4b7031cb594b2c728a3a0b2c7c6a84036efd3c602176a4206a65e257ceff31d20bf39ccbdb6990240919688a79aa2eab835a2b79f4d063fe0d84a1b435265b86fad502982f1dab135d5b0488b0c242bc499e208a7", + "expected": "274cb645a0bd6bde", + "note": "random" + }, + { + "n": "0xb41921aa0398956c418d929267cd2080c1057cf4f5e5bf5acd525548d5491079d1e5da691a1d8da11960c021b5cc9eabad943d6db50c0a8e1bfd5d6cc19c7110c5163cb5e8e66d402e1b017b15cb709096ff87b75c3eb0f1236ee8c2dc1a56a2065b937", + "d": "0x1a77e1fd8ce3612b2491fccb629bfff3036fce7ff79a6ed9d16b1517d7ee90c863e52824339364d06f914bcb48df533c72b", + "expected": "591b379a1f6338e0", + "note": "random" + }, + { + "n": "-0x61de8049d041a6b9295f5b1d5f21b12525ad1e40c84282bf905aba0c9a17bd266117b0266ace13a75473976b139d62efd0ecfe61142e4c1381c1c292822b9841e23a793c47cebaa2f73b8d93426343eb4eba9eaef212b34628add69d6747e468888986d19e9", + "d": "0x1098d482cc6dea9589e3a89ebb2b1fa001a33eb9d32e229df065c2578d657c866294ab44cf8cb67ec422fea0d9208801a85d0dfdf957e8e15771eeda1d515f451fbc71279a74b2da87b6117a3221584f0002991b85ebb7a83df6c4e1a8c8b8abec22e1d8c8cb78a61ca94a48c90fcdb0a2df07dfde759084ece521288a1009a6b97426286d1a26a3cf592062639fc5", + "expected": "ab579652b8051ae7", + "note": "random" + }, + { + "n": "0x17061c40d57c4792e72e7b04b6daf3801450b909c6a6b4de5f0a632fc8e744352d5c5fadcd74144d879a80a05e1aff16473b5d3ce9ba657411e1f3a916db940e5", + "d": "0x6aa95b0e83a77ec1119a56db453467c1dbd48755e4ef689cd90bae19ad89b8d9aaf57800ea5dd87899779c81b838312cebe3e4e8bc5b8237870ef4bf3a91", + "expected": "410ba145a324da57", + "note": "random" + }, + { + "n": "0x6f550bcb7a4337a98416aa9ffdf9dae2a9bfdbd752fb9f27e1ca7b72c9b47fccc04512815816889a255c80072ebfdd82c4abaf57b124216876da1ac25d570db678e741e2c0ef2169782aa81bdfa9a3bbab5966d04b7fc289e6d0230cf79bc9bef6cdc9103e33a40f1bfb018393c5bcf090ac301d75e4ef0823dd603d599e753865025cdf5ebca0eb40eaaf2984748d7facec01492f", + "d": "0x74092e46e9a07cdb4eaade30d01ce804a571e61f0e23680160f8d3b5b8beba9ca0115", + "expected": "792eb3ed7584ff45", + "note": "random" + }, + { + "n": "0x19c9a0d750e8d029a1a8afc4a8f0d96f7b5ab607e66857cbed6ae3ce982478c3358f8cd1b6fc3cc25983d6389190182e6ad30a1b48dfc8101", + "d": "0xf55040d949aae157b0e6b1a1b818f3b72f6a91fbc3a3dbb6f545649b34e9acba66c7033a7eb3247c47926dd6660619", + "expected": "447ae9367c7e62ca", + "note": "random" + }, + { + "n": "0x10a50542155a5556953d56c0c0ba90a9c7a3369becea1d2c3cc6c50c554df844e65755f42fd7fedfaf72d87cbf2f5198b8fa3b1956299590ff2ddfdf6306f7e9893b8039bd2d5428a9b8f24630eae538063dd556722cb0f67f6545911bc36dcc44b9e9285a049", + "d": "0x3aed4215ce5f3d47081b95b2e443ff41a8e870dea18f824047652beb231a19add82008fd3087c4699ac51f57689e7872104b0460867cac15c6fe9eaf3b3eeb7b2ddb367ae4e1ac87a5913ff0eb7c88294129629", + "expected": "495213ddd1ab6df7", + "note": "random" + }, + { + "n": "0x169def832b6efc9d79fcc55c9def6236a15d57829ba9f1e91efc0e957f7124e1ea44474b1418450ce975050e9dbcb9ebf6d0203cdf9a84bb9fd4966d9a82d39dcab2ae2f4465567a074dce8e01cb141e162747ad4819f054f6134f4ba81", + "d": "0xe9a532d78974115f509a89d0e53b1e9aa29c4178e33e478ea673d15a3d1163039baa2c881105850eaa79c0132a020525ea66f3fe6fef6fc2bb784210d7cd20fd890a7a9aa1b39dea0bb1f6623faea3100775f62da14ea8109e4cb22ab35e1d1eb231abe30f62c1f2c4c39d23", + "expected": "3878c7e78ca87e9c", + "note": "random" + }, + { + "n": "0x4d1d81cf", + "d": "0x1b06a0c7ff9fa52c02a4235036438dc761255", + "expected": "38c6d3bb548088aa", + "note": "random" + }, + { + "n": "-0x450c4c635cd1a32fe08361fc92d0aaf01524db7a92cb0b7abfe3", + "d": "0x8551eef3ebace716026d0c1fe9c82f53f21376ba8e28af5805acb7227ba4c81a04d0f4dd7452c0631a1474b32ae306630ba6fa4b898bcf5f3a703767965a249471c51b2c24c1c8ba7d299b7b7bec7bc6707ba190dd61d115f8920b142da0fd1487bf1949dac83b35a4df823de1034770b27e95bd4d4fe40b42e64790d4ea2aef1", + "expected": "8ca092bbf5e047e2", + "note": "random" + }, + { + "n": "-0x21820ba6178f1f4dd8c76ebb55dddc36666eabffc0bd36337a26b72c6f6341f22792b43ce9072bb4aa79dd50ca3a6dd9f0b12a78f2e3b3119ac9bc2e80bc52fdd7bd1", + "d": "0x1a4b1490c05505d9bad97a19da55486eb5c450348bff065fc8a5345b16e94d90b20397c67a58ccca09e173338bb86bcb4e9b0b315d98043b8ce938a30578639f5568e42d75105887125a13ebd6ab5b5152679143d37931a4563e34e6a0cb19f149a5636b286ec6a9076b6b1c7b52426fe95a801129f5d6e29d", + "expected": "a4b463ea835a107d", + "note": "random" + }, + { + "n": "0x43f79016b5ced686cb6b646261e7cc6e290a76896ae86328e103e00187a9d0baf7e775ff3e7b571437bcdae414d0cc8b7d317571787decbc1e74d4b269236d15e16795330115f60da7956acf2be1afaa2931fa09938663369b46472c7d44a97301f450af14368ec4a7970d44ac28a5a85df3f84d", + "d": "0x17f02efc2d24b6a0324e033361ae56a2ce0dd4415c0a4a3df8617e443b01b6a4e4e4185153549c79c677cec4ec9b375c7ffcda79005703d43145511c7dcbdfb7efda5975afae15b2f3db0e85dc4eca5f6ff58d9c9460f7605aebb8c3f8a4afb", + "expected": "4a46b6d2c702eac0", + "note": "random" + }, + { + "n": "-0x8382e4b8a37a65c1b6fed410085c540cb8e5e7e5f5111780923bd4b96747c323317e89a29aa5ea86117470968342629f38fb2fb57d9b455b60979571e08dffce5272e1", + "d": "0x3c3b6bec213789f3ec9303b2b1697a1875bcb1", + "expected": "d801779ec8d0b850", + "note": "random" + }, + { + "n": "0x193db6b14ea4c669b0133781d4c4797c4511fac932c7bca02b2bb95ddbdd91f6b64987317aa6d730c02020d7c194e1ca0c015c02662ac415619f94e4a8df149e996cdc854e77b6e4725c774f01", + "d": "0x161338647da32c425b2df06aa74bac2fb436f8ce69a7b4c7178391aa3d2d0ed2e6a2eab78da822986877f06d9759f90365ba4cabdf91fc961f0b0f0c325ef59ec75e97b6777bfc53041c1c8ab618cd161889610d77f036b907444dd19304a23badb55c95d3a793b0498e3ac9346fd952e7cfd5c405e92b5dc207bf4e9c6d7d7f29cc1e3679d19", + "expected": "23324b71e80f6d57", + "note": "random" + }, + { + "n": "0x1a26fdb076fc4e509c4d3482b069dafefe01fffe9e9e9ff957ead9357115830fd5f36ce2b1fc78fdba81a0d3cb28579bb28a07c211d9fbf9522b714274d415deb330d144023410ec8138134a87d086b3b4f7f", + "d": "0x19d91fa10e44f895d4f9e427a6332169f90ab006994b20df5ca610c6d03db48ca0feced9b0b98282271521064e92d09c464ceddd54d6d9836e17e342ab146c89d1be1855f97e1383347507688d078c5dc3", + "expected": "40b030332f510bcd", + "note": "random" + }, + { + "n": "-0x39fbf57683454d552202dc1f1c4e9e7d850020c008826b5709e6fad8b0ee3fd4b0143c9", + "d": "0x1fecc7314f94c26af0d14be5bcc2e37cff869cd43718d65efd01e67426ab1f6260b6c51769ec181301cc25c3cc06eab463dbf92b335649132edd01de44596373fe163fae3ddb22cf88ae33c63ef7e2d5d2f95d98856deb9df47", + "expected": "a4fd0f6f7c2a08de", + "note": "random" + }, + { + "n": "0x130a9991a599282e4d1c244e51be08129951a9516b655", + "d": "0x159d683ac18b0f0607f74908c24f0f89ef5e5a25ff84a434033679c17c250bd354c1e9f1c04e200525ec22eba2177e2402d66587f0c28f7ad81cacec566444c208ffd4f359dde1bd6b32cc7e825be870c5db43f4819142cb5a53", + "expected": "1e2c30a9275b8295", + "note": "random" + }, + { + "n": "0xc35b2b5418a144dc46913e7f4bc7aebb4748b61840237e66e4c185397ac3b97f3d1c1a5a7fee724af495109c7af12b48988326824348d8b8fe1f6e5e6edbbcc9fde2402c0d485582f2fd2e4bcaa072fdf9d4d80fb70cbb5d451b629ca2abc6b86f9022a6f84be56d", + "d": "0x3808c32bc0abc80682442baf7a8e27455051284e8712263701fa4a4b200b41f770855859", + "expected": "620be416af9a0d23", + "note": "random" + }, + { + "n": "-0xe012225fe5169c5eb0433b939842d1756118a6483281", + "d": "0x635897f645cc51104fa46f86914683461679993b24e2162e10ee95d1ddccf87d51ef9047b8e1d0176b1c497602b03f5ef07470022a1b7732f432be0643d60becebd28cf26ce4000744daf921481c182c4576af38ac7907b215681b90cf19761927b13853857c701cf1bcda24961f2052e0bd4acb795d80603f848b66e6c9788191452d612a94ded93a8ca005fe10930c3b7251", + "expected": "81820b2d4ecb7a65", + "note": "random" + }, + { + "n": "0x75f610271aaa7ac33ece74ca568efa962eb1aa1a8276e1549a5cdac89c55734dcba97112f166cd", + "d": "0x4d14797b787a430284e1b", + "expected": "4e387c694b26fe4a", + "note": "random" + }, + { + "n": "0x57223bf42a1e6e19ca7212605803f08ce6ac54b2a93bad36d0fb6194912929c3207", + "d": "0x4f048a496f42127eeca798f784e48929c4133facd6dbdd294596c01a9a4687ff4e6057f1c8622818387abb2b0e77dd23ac8b35d6e10c3dce5806ca3eead7c9b13c0a6d3063026ec831139694547ec7f9b1481338d02030f18c7c87c11c5f26953a3100f9fa77a65afc9d24048ba1716f18de3", + "expected": "1771a4b44fb405a2", + "note": "random" + }, + { + "n": "0xcd808ad850d1036d6a090ab9b2be888e4090cff293", + "d": "0x7dcc37c524f52b509b408e5abf72cf088bcd2a43014564bac7c84a3e3c044c39484af650f3aaf903b8a0d706e4e5d9958e7da08762211c3ec48ebf4aab57e2adb57bc943450a0b9c5ca9a08c2506d7dce4b87e654b51eda9b4ebf7cc39b711e730db0460495b", + "expected": "177a233107b3413d", + "note": "random" + }, + { + "n": "0xe8a324ccbd88b1d0949e8e00d40df4ccedcdd7e0335b092e15b279c1be780c319be4f1e12563c6e68b996bbbaa7c4b54e22f7f2318e5506a0de32dcb2539ad100564f794a496ab35e9f733048a84b203fc60d1e18351fb64eec49caa7fdb8f278894a59fe4017bc5783cd6e78cb637bb4fe46cc8223b589b9784d78164bdc567d09df03b2779", + "d": "0x16ecacd7638534f6d8110f86b27", + "expected": "7c644bc2d1b72158", + "note": "random" + }, + { + "n": "0x18fcc3d387e6ce6cee294a9b163ecb50100119ed5efdb1d4de927d984887296f905eafcf001851cb7ebe71696c34a7a0eee00c8aca375605f26a5f3cc7e098bae994a05bd11488a714ce3350ee5b5f2ba5e18516d2416c058dbfae046ca8860c4743594ec43a339753fbe444ede2ee20b308834da94497880474b7de7cf6cdb6e1917de954d7baef2071dbca7f", + "d": "0x541dec6c6386b22d963ac931e6bab2c61e76c68b9eb927993df2683068b931e16414f6c558803d6e2528795058e0a9f8d7284082c50826675250fd03039c7dd4482977dd9a74b8a36a26944d6eafd150e8567b38e63ce0ceb20f74933207a155a7b9e785fe211bc4d", + "expected": "521302f4016e6d11", + "note": "random" + }, + { + "n": "-0x3b5a71efd25ec34ef7eb4bc9dc80f705f7f98c9e15ed05f036d52f561177c3a565e0798a2f56d3c4281b7f2079ac9e6e0880dd780b9f52449b48b6e5639cf5fc9474e08b76f2f3092ab057e9848b8004c4647109190a69596ba45c1545bf45ab93b30f10a1cdedaae640678b64ee1de9cfa2ecf4489852ce49e3", + "d": "0x798fa64a59ca99a138952f3798d60726af26fcf19f341b1d910db9cb5fa3cf6c5a26dd9f9d2450cf41c8b30ab852d7672fcf4229a1bf96036ec63aa536896a27e5c687b4d2811fa2960344109de62f6474ec7a0e51e77aa9db6427fe89dc6751ac5762675ddf93362757360579c02fdc7e806fee3da78b47fe49a9e294793eeb", + "expected": "bcdf3fa2262bf299", + "note": "random" + }, + { + "n": "0x3686e27f3411b7a11453d25dc3c9ed7382dc1e3362b98aa4734f27db838a3fe490b7ec8ea8fbaeb91b16575403d67adef4aefe226fee981600bd3370b1335f1994eee009f9772259f5bc8f5c456225986a18727e91ead25d12888d108c5cb6eef00749737e289ac0b8e2396041a014092187874d843ce197590e240599839806d2f58296cbc5efa2186dde3f", + "d": "0x1860e54904dc811893c2dbd6c7eb95b7d4ba626a7645efdd26b7effa55180e56601d56d5b6544884a291b4d6f1d7", + "expected": "6f01e4b856e634b3", + "note": "random" + }, + { + "n": "0x159ac29fc71bf314a5d124ded46b1d275d20de96ed8be2dc91a569febc9b8dc1558be9", + "d": "0x17d6d378bd3408b3fd650262f11eb14a471b7ad0f0201dc6a52bb4948f79cc1939d7b489731659d2c601aa6d1a6406976464d166c692800d8246974de189c63c741a7479b5a4c4da883d08fecb10a777e843c761b0bcd037432ff46fe15a6c22f901d25bdf2d0566d1c9d24192977798d973851df629093d0713816f9a60ddf5cce4f1dc01cabacfad163a03e4837e7bacd47", + "expected": "082d00197983b5bf", + "note": "random" + }, + { + "n": "-0x31f0eef7a462213df0c17f6c4021def7069cd27407d774e4a558f214b2676a62a7fec3be910b6b5e02b81068a73e1d865127", + "d": "0xc52e382d562699efbaf59938d42f849a90f7f3cf40200f49e190905", + "expected": "cb1035ad8898f340", + "note": "random" + }, + { + "n": "0x1b66c94dcd6c65ad5b11b3df7b8f0c88a008ef8bbbd401dd", + "d": "0x3f243051def4a9166c48a8c146bbabaef3edc9cd24c1d250d9e2ab25dad64a3dddb9f4cfdef1a7badf9142d10819ba1885e75db71ea37dbc347b8a23e26d6447", + "expected": "2bdbc62d924a8ba6", + "note": "random" + }, + { + "n": "0xced3848cf7c8cf52f4053e0cc01993560793fec142ed360ebf54b7822d6bc09c2a48e8244acae8d9c0dfea2531b0038f0745107a159ece4da4d5dc123a06461e93c3f884aa388dbce579576b7e69ea4342f97d2301ad9de1444c12f0512eb7278063fb118553828d89e0bf3909cfcdc2086d70fe5ca5d854ace9207e04b8aff64ff7ed83a79cd280095", + "d": "0xfd12e795e5a24b77d32bab083cea5bed124579ee12d202f1d3382d0cd78a5feb7e21971e4c889abd6043ef8d4ecd8d92f96ce4686133575eb845d5", + "expected": "672a26f710f9a0e2", + "note": "random" + }, + { + "n": "0x2f09b6607a2974789ecea0a0788786029cce7cf0bce0ffffa7e7e0873f15c3f73aa07721744ed", + "d": "0xc8b6af9264cd8545edcf8cb27bf4743dc8aec15bbf293310b0fe72ac0ca1429fcc5bd93fe0b54c6eda998af2749603dade259833", + "expected": "390dff4bab39df74", + "note": "random" + }, + { + "n": "0x59007c33dc3319a3c178ade0ae0ca8918fe960187b3546030c22c0d5b44515fd2cf34cb93002f1be05a9baa27f6346aed01ac837610093b7ad3ac5bcc627b835c3eaaba4e07db77d51119bd47fc834b4ca77a1850f945a261b94d0fdd8add68e45de690e626815e28a7f", + "d": "0x1b5c34d55828b97cf11748f6f981e4b098d38bbe02837afa04c05509a42dd1acb36eb30fd237a561b4c998767d", + "expected": "5e8a06160765aaa4", + "note": "random" + }, + { + "n": "-0x6521d8b869768a70266afaa5ff7db1187ba22badeab9830503c2b087989", + "d": "0x5d3579cc5b91ea57ebd769ce705434325b713", + "expected": "c5715c2f58e4db2a", + "note": "random" + }, + { + "n": "0x178a9cb720211acabd0eaaeb2e84e49df4d1285fb6b0b1d8721fcba6f1d7d7c83d43ef759bb5770216a7ccb0882a47d712aaad0c84b40872f60766be10a0cc8394c0fb5e27669f53cb86fc0ae61faf85475038d", + "d": "0x54d96fe35bfc5f2db58adf25a4ee8f2bac6dd28530881a536e9b98d40f", + "expected": "5b11c1bd8a2f5dc4", + "note": "random" + }, + { + "n": "-0x1f2049e2d9f98d0ddaba306856ba2d628a25eddeca0a44256d5a2d85ab8e12de3b4f6bd871add4ebf30dc9511706e24548663461f48ac8266992cf4f4da75f02a8190ea7fc2fa928a4a59b9c5b2a6340ff11c23aba637b4fed795cfda24688a415f06269240c990cc1ed988b2f284398ebc31475ca467fa74d43f8c60eb05601831569f4a801", + "d": "0x7f699f50e01feac990f456b01b937cce8a71811d68a0ade9c6a1b4c3146547f1aef90472e9e652f", + "expected": "ef0f450664e529fa", + "note": "random" + }, + { + "n": "0x18b42986e7bf4f123edca00844801b3030ee405789fc4bfeb26320e0e59de7415a4dd8b194ab6fe932d63d39ea70b872a817a268701b6199df750b105e9794a4311b1b0e17ea389ddbee3dfde031f283704fc980c8505d5b0ddc6f671ce191251dcfabf153f69", + "d": "0x2e9df873be85fa8a7bc7c6f8edefad622ca9811a8745a18e7eefb6bf9c0acc8e9a45d2e4edd51ce7", + "expected": "5f20f52e9158cd44", + "note": "random" + }, + { + "n": "0x64e5a89605396282bb5c310b5d2318eaa1c8e5d902654b369c65bc56c73cdafb42a7e260f14d8820ad2e66acb69f4697c0fa90d6399581312c31554afe9a0aeb828896d365cbe12d571cd5dd5a2b1aa92cde97886af8f8ae38f0e8c6d65945982453144368b8137a20183263c3720c1c76f", + "d": "0x8c34e8bf7c95a3d0457616a7b4eb652bfcb197b861150c92633c56f4ad69fa7023282f58d661d43260faaea35cc84b7289f3f62e06a3bc098b867aefd5b8bac3a78f35fa309db8e8314896684abab7d0cf1719053482908d655d0d89f67b0efa52ee9a4b48961318571d5b8b3", + "expected": "42670738153311e5", + "note": "random" + }, + { + "n": "0x63b65fec2585f66cb9e032ee01fe5fddac9b7c70e0e173f4ed9356f39659da0f971db", + "d": "0x73a6ac69f2814e6370db3efb88463872b42ad3de40a9a4432c31cc2182ecd3f93ac90016002811aba5c8d45fbc1b0f45cccb1969d9ff69f06fa5393e04c49ace528e42a02fc75430d", + "expected": "2ceb970118570abb", + "note": "random" + }, + { + "n": "0x278fec48da94648c17bdb9fe2175674295c9d692b55682491435553e3915a7d16453b48ca094d158ed95cc8df4b1131b705db8eaf2b708e523e88f7aa638c58bd2ea5bb89594b5ab79d27c2de40154ebe996f28ae7a91c995317ff805893c69d07357620ac4b70017ab3adbe49365644ba1540a400c06f90b51207f1baee0db", + "d": "0x1d8f2c5547c55a7", + "expected": "7bf56a18ab4b1860", + "note": "random" + } + ] +} diff --git a/test/contract/fixtures/tofloat_cases.py b/test/contract/fixtures/tofloat_cases.py new file mode 100644 index 0000000..d2f0cf5 --- /dev/null +++ b/test/contract/fixtures/tofloat_cases.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# Generates tofloat_cases.json: Fractions n/d (hex integers) and the double nearest +# to n/d with ties to even (IEEE 754 round-to-nearest), as reference for +# ExpressionEngine.toFloat. The reference is Python's float(Fraction(n, d)), an +# integer true division that CPython rounds correctly; OverflowError means the +# rounded value is beyond the double range (+-Infinity). +# python3 -I test/contract/fixtures/tofloat_cases.py > test/contract/fixtures/tofloat_cases.json +import json +import random +import struct +from fractions import Fraction + + +def expected(f): + try: + x = float(f) + except OverflowError: + return 'Infinity' if f > 0 else '-Infinity' + return struct.pack('>d', x).hex() + + +def case(n, d, note): + f = Fraction(n, d) + return {'n': hex(n), 'd': hex(d), 'expected': expected(f), 'note': note} + + +cases = [] +M = 2 ** 53 +# ties and both sides of the midpoint between neighbouring doubles, with +# numerators and denominators beyond 53 bits (bigint path) +for k in (200, 1000, 1100): + D = 2 ** k + half = 2 ** (k - 53) # half an ulp of 1 + cases.append(case(D + half, D, '1 + half ulp: tie, to even (1)')) + cases.append(case(D + half + 1, D, '1 + half ulp + tiny: up')) + cases.append(case(D + half - 1, D, '1 + half ulp - tiny: down')) + cases.append(case(D + 3 * half, D, '1 + 3 half ulps: tie, to even (1 + 2 ulps)')) + cases.append(case(D + 3 * half - 1, D, '1 + 3 half ulps - tiny: down (1 + 1 ulp)')) +for k in (60, 300): + t = 3 ** k # non-dyadic denominators + cases.append(case((M + 1) * t, M * t, 'tie (2^53+1)/2^53 scaled by 3^k, to even')) + cases.append(case((M + 1) * t + 1, M * t, 'above the tie, scaled')) + cases.append(case((M + 1) * t - 1, M * t, 'below the tie, scaled')) +cases.append(case(2 ** 200 + 2 ** 147 + 1, 2 ** 200, '(2^200 + 2^147 + 1) / 2^200 (review example)')) +cases.append(case(10 ** 310 + 1, 1009, '(10^310 + 1) / 1009')) +cases.append(case(2 ** 1200 + 1, 2 ** 2200 + 1, '(2^1200 + 1) / (2^2200 + 1)')) +# subnormal range: quantum 2^-1074 +for num, den, note in [ + (1, 2 ** 1075, 'half of the smallest subnormal: tie, to even (0)'), + (1, 2 ** 1075 - 1, 'just above half of the smallest subnormal: up'), + (3, 2 ** 1075, '1.5 x smallest subnormal: tie, to even (2 x)'), + (5, 2 ** 1076, '1.25 x smallest subnormal: down'), + (2 ** 52 - 1, 2 ** 1074, 'largest subnormal'), + ((2 ** 53 - 1) * 3, 2 ** 1075 * 3, 'between the largest subnormal and 2^-1022: tie, to even (2^-1022)'), + (1, 3 * 2 ** 1022, '1/3 of 2^-1022 (subnormal, non-dyadic)'), + (1, 10 ** 330, '1e-330 (subnormal)'), + (1, 10 ** 400, '1e-400 (below the range: 0)'), +]: + cases.append(case(num, den, note)) +# overflow boundary: MAX = (2^53 - 1) 2^971, the midpoint to 2^1024 is (2^54 - 1) 2^970 +mid = (2 ** 54 - 1) * 2 ** 970 +cases.append(case(mid, 1, 'midpoint MAX / 2^1024: tie, to even (Infinity)')) +cases.append(case(3 * mid - 1, 3, 'just below the midpoint: MAX')) +cases.append(case(3 * mid + 1, 3, 'just above the midpoint: Infinity')) +cases.append(case(-(3 * mid - 1), 3, 'negative, just below the midpoint: -MAX')) +cases.append(case(10 ** 400 + 1, 3, 'far beyond the range: Infinity')) +# random large pairs (seeded), quotients across the whole range +rnd = random.Random(20261101) +for i in range(150): + n = rnd.getrandbits(rnd.randint(1, 1200)) | 1 + d = rnd.getrandbits(rnd.randint(1, 1200)) | 1 + if rnd.random() < 0.3: + n = -n + cases.append(case(n, d, 'random')) + +print(json.dumps({'generator': 'test/contract/fixtures/tofloat_cases.py', 'cases': cases}, indent=1)) diff --git a/test/contract/mathjs-version.test.js b/test/contract/mathjs-version.test.js index 48b3040..8d5a0a4 100644 --- a/test/contract/mathjs-version.test.js +++ b/test/contract/mathjs-version.test.js @@ -1,44 +1,44 @@ // Contract points that depend on the Math.js version (0.1.17 / Math.js 9.5.0 and -// the current Math.js of package.json): precision of the arithmetic helpers -// against exact rational arithmetic, numbers of large Fractions, Units and the JSON form of 0.1.17, and -// the documented behaviour changes of Math.js 10-15 for user expressions. -// Version differences are asserted explicitly for both versions (no skip); -// mathjsMajor is the major version the tested artifact must contain (load.js). +// the current Math.js of package.json): exact Fraction arithmetic, toFloat, +// Units and the JSON form of 0.1.17, and the behaviour changes of Math.js 10-15 +// for user expressions. Version differences are asserted explicitly for both +// versions; mathjsMajor is the major version the tested artifact must contain +// (load.js). 'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); const {ExpressionEngine, mathjsMajor} = require('./load'); const {assertFraction} = require('./helpers'); const N = ExpressionEngine.toNumber; const legacy = mathjsMajor < 14; // Math.js 9.5.0 of 0.1.17 - -// exact rational arithmetic with bigint, independent of Math.js / fraction.js const B = v => BigInt(v); const babs = v => (v < B(0) ? -v : v); + +// exact rational arithmetic with bigint, independent of Math.js / fraction.js function rational(f) { const m = /^(-?)(\d+)(?:\/(\d+))?$/.exec(f.toFraction(false)); return {n: B(m[1] + m[2]), d: B(m[3] || 1)}; } -function addRational(a, b) { - return {n: a.n * b.d + b.n * a.d, d: a.d * b.d}; +function sameRational(a, b) { + return a.n * b.d === b.n * a.d; } -// correctly rounded double of n/d: 25 significant decimal digits, parsed by Number() -function rationalToFloat(r) { +// approximate double of n/d from 25 significant decimal digits (relative error +// below 1e-24); NOT correctly rounded, used only with tolerances +function approxFloat(r) { if (r.n === B(0)) { return 0; } const sign = (r.n < B(0)) !== (r.d < B(0)) ? '-' : ''; const n = babs(r.n), d = babs(r.d); - let k = 25 - (n.toString().length - d.toString().length); + const k = 25 - (n.toString().length - d.toString().length); const q = k >= 0 ? (n * B(10) ** B(k)) / d : n / (d * B(10) ** B(-k)); return Number(sign + q.toString() + 'e' + (-k)); } -function relativeError(value, reference) { - return reference === 0 ? Math.abs(value) : Math.abs(value - reference) / Math.abs(reference); -} -// deterministic generator of bigints with up to `bits` bits +// deterministic generator of positive odd bigints with up to `bits` bits function generator(seed) { let s = seed >>> 0; const next = () => { @@ -61,8 +61,14 @@ function generator(seed) { const reviver = new ExpressionEngine().getJsonReviver(); const fraction = (n, d) => JSON.parse(JSON.stringify({mathjs: 'Fraction', n: String(n), d: String(d)}), reviver); -test('arithmetic helpers keep small values (regressions of a denominator bound)', () => { - // 0.1.17 gives these exactly; an absolute bound of the denominator turned them into 0 +test('arithmetic helpers: x + 0 equals x (exact arithmetic, regression of a rounding policy)', () => { + const x = N('1208925819614629174706176/3'); // 2^80 / 3 + const y = ExpressionEngine.add(x, 0); + assert.equal(Number(ExpressionEngine.compare(x, y)), 0); + assertFraction(ExpressionEngine.subtract(y, x), '0'); +}); + +test('arithmetic helpers keep small values', () => { assert.equal(ExpressionEngine.toFloat(ExpressionEngine.divide(1, 1e20)), 1e-20); assert.equal(ExpressionEngine.toFloat(ExpressionEngine.add('1/100000000000000000000', 0)), 1e-20); assertFraction(ExpressionEngine.multiply(ExpressionEngine.divide(1, 1e20), 1e20), '1'); @@ -72,37 +78,7 @@ test('arithmetic helpers keep small values (regressions of a denominator bound)' legacy ? '0' : '1/10000000000000000000100000000000000000000'); }); -test('arithmetic helpers: the mean of many evaluated payoffs (probabilistic sensitivity analysis) stays bounded and accurate', () => { - // sd-computations 0.6.1 prob-calculate-step: expectedValue = add(expectedValue, divide(payoff, numberOfRuns)) - let seed = 42; - const draw = () => { - seed = (seed * 1103515245 + 12345) % 2147483648; - return seed / 2147483648; - }; - const runs = 500; - let mean = N(0); - let exact = {n: B(0), d: B(1)}; - for (let i = 0; i < runs; i++) { - const payoff = ExpressionEngine.eval('2500 * pr - 250 * (1 - pr)', true, {pr: 0.1 + 0.8 * draw()}); - mean = ExpressionEngine.add(mean, ExpressionEngine.divide(payoff, runs)); - const p = rational(payoff); - exact = addRational(exact, {n: p.n, d: p.d * B(runs)}); - } - const value = ExpressionEngine.toFloat(mean); - const reference = rationalToFloat(exact); - if (legacy) { - // fraction.js 4: doubles, each of the 1000 operations off by up to 2^-52 - assert.ok(String(mean.d).length <= 16, 'denominator ' + String(mean.d)); - assert.ok(relativeError(value, reference) < 1e-12, value + ' vs ' + reference); - } else { - // exact while numerator and denominator have <= 64 bits, otherwise rounded with a - // relative error below 2^-64: 1000 * 5.4e-20 = 5.4e-17, plus 1 ulp (2.2e-16) of toFloat - assert.ok(String(mean.d).length <= 20, 'denominator ' + String(mean.d)); - assert.ok(relativeError(value, reference) <= 4.5e-16, value + ' vs ' + reference); - } -}); - -test('arithmetic helpers: random operands of 1e-30 to 1e30 against exact rational arithmetic', {skip: legacy && 'Math.js 9.5.0: the operands (up to 160-bit integers) are not representable in fraction.js 4'}, () => { +test('arithmetic helpers are exact: random operands of 1e-30 to 1e30 against bigint rational arithmetic', {skip: legacy && 'Math.js 9.5.0: fraction.js 4 keeps numerator and denominator as doubles (not exact beyond 2^53)'}, () => { const g = generator(7); const ops = { add: (a, b) => ({n: a.n * b.d + b.n * a.d, d: a.d * b.d}), @@ -111,7 +87,6 @@ test('arithmetic helpers: random operands of 1e-30 to 1e30 against exact rationa divide: (a, b) => ({n: a.n * b.d, d: a.d * b.n}) }; const operand = () => { - // p/q * 10^k with 1-80 bit p and q const k = g.int(61) - 30; let n = g.big(1 + g.int(80)), d = g.big(1 + g.int(80)); if (k >= 0) { @@ -119,89 +94,76 @@ test('arithmetic helpers: random operands of 1e-30 to 1e30 against exact rationa } else { d *= B(10) ** B(-k); } - if (g.int(2)) { - n = -n; - } - return {n, d}; + return {n: g.int(2) ? -n : n, d}; }; - let worst = 0; - for (let i = 0; i < 2000; i++) { + for (let i = 0; i < 1000; i++) { const a = operand(), b = operand(); for (const [name, op] of Object.entries(ops)) { - const r = ExpressionEngine[name](fraction(a.n, a.d), fraction(b.n, b.d)); - const e = op(a, b); - // relative error of the Fraction itself, computed exactly: |r - e| / |e| - const x = rational(r); - const errN = babs(x.n * e.d - e.n * x.d); - const errD = babs(x.d * e.n); - if (errN !== B(0)) { - // <= 2^-64 (with the sign of e.n accounted for by babs) - assert.ok((errN << B(64)) <= errD, name + ' relative error above 2^-64'); - worst = Math.max(worst, rationalToFloat({n: errN, d: errD})); - } - assert.notEqual(x.n === B(0), e.n !== B(0) ? true : false, name + ' kept a non-zero result non-zero'); + const r = rational(ExpressionEngine[name](fraction(a.n, a.d), fraction(b.n, b.d))); + assert.ok(sameRational(r, op(a, b)), name + ' is exact'); } } - assert.ok(worst <= Math.pow(2, -64)); }); -test('arithmetic helpers: a positive sum of 300 terms between 1e-25 and 1e25', {skip: legacy && 'Math.js 9.5.0: see the previous test'}, () => { - const g = generator(11); - let sum = N(0); +test('arithmetic helpers: the mean of many evaluated payoffs (probabilistic sensitivity analysis)', () => { + // sd-computations 0.6.1 prob-calculate-step: expectedValue = add(expectedValue, divide(payoff, numberOfRuns)) + let seed = 42; + const draw = () => { + seed = (seed * 1103515245 + 12345) % 2147483648; + return seed / 2147483648; + }; + const runs = 100; + let mean = N(0); let exact = {n: B(0), d: B(1)}; - for (let i = 0; i < 300; i++) { - const k = g.int(51) - 25; - let n = g.big(30), d = g.big(30); - if (k >= 0) { - n *= B(10) ** B(k); - } else { - d *= B(10) ** B(-k); - } - sum = ExpressionEngine.add(sum, fraction(n, d)); - exact = addRational(exact, {n, d}); + for (let i = 0; i < runs; i++) { + const payoff = ExpressionEngine.eval('2500 * pr - 250 * (1 - pr)', true, {pr: 0.1 + 0.8 * draw()}); + mean = ExpressionEngine.add(mean, ExpressionEngine.divide(payoff, runs)); + const p = rational(payoff); + exact = {n: exact.n * p.d * B(runs) + p.n * exact.d, d: exact.d * p.d * B(runs)}; + } + if (legacy) { + // fraction.js 4: doubles, every operation rounds (up to 2^-52 relative) + assert.ok(String(mean.d).length <= 16, 'denominator ' + String(mean.d)); + assert.ok(Math.abs(ExpressionEngine.toFloat(mean) - approxFloat(exact)) / approxFloat(exact) < 1e-12); + } else { + // fraction.js 5: exact; the size grows with the number of runs (hundreds of + // digits here, thousands for 1000 runs): the aggregation in sd-computations + // has to bound it + assert.ok(sameRational(rational(mean), exact)); + assert.ok(String(mean.d).length > 100, 'denominator digits ' + String(mean.d).length); + assert.ok(Math.abs(ExpressionEngine.toFloat(mean) - approxFloat(exact)) / approxFloat(exact) < 1e-15); } - // 300 roundings of at most 2^-64 each (no cancellation), plus 1 ulp of toFloat - assert.ok(relativeError(ExpressionEngine.toFloat(sum), rationalToFloat(exact)) <= 4.5e-16); }); -test('arithmetic helpers: small results stay exact', () => { - assertFraction(ExpressionEngine.add('1/3', '1/6'), '1/2'); - assertFraction(ExpressionEngine.divide(ExpressionEngine.subtract(1, '1/7'), 3), '2/7'); - assertFraction(ExpressionEngine.multiply('2/3', '3/4'), '1/2'); - const limit = String(Number.MAX_SAFE_INTEGER); - assertFraction(ExpressionEngine.add('1/' + limit, 0), '1/' + limit); +// toFloat contract: the double nearest to the Fraction, ties to even (IEEE 754 +// round-to-nearest), also for subnormal results and with overflow to +// +-Infinity. Reference: fixtures/tofloat_cases.json, generated with Python's +// fractions.Fraction (fixtures/tofloat_cases.py). +test('toFloat is correctly rounded: ties, both sides of midpoints, subnormals, overflow, random large Fractions', {skip: legacy && 'Math.js 9.5.0: fraction.js 4 holds no bigint Fractions'}, () => { + const {cases} = JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures', 'tofloat_cases.json'), 'utf8')); + const big = h => (h.startsWith('-') ? -BigInt(h.slice(1)) : BigInt(h)); + const double = hex => { + if (hex === 'Infinity' || hex === '-Infinity') { + return Number(hex); + } + const view = new DataView(new ArrayBuffer(8)); + view.setBigUint64(0, BigInt('0x' + hex)); + return view.getFloat64(0); + }; + assert.ok(cases.length > 100); + for (const c of cases) { + const value = ExpressionEngine.toFloat(fraction(big(c.n), big(c.d))); + assert.ok(Object.is(value, double(c.expected)), c.note + ': ' + value + ' expected ' + double(c.expected)); + } }); -test('toFloat of Fractions beyond the double range of numerator or denominator', {skip: legacy && 'Math.js 9.5.0: fraction.js 4 holds no bigint Fractions'}, () => { - const ten = B(10), two = B(2); - // not reducible: the quotient is in the double range, numerator or denominator is not - const cases = [ - [ten ** B(310) + B(1), B(1009)], // 9.9108e306 - [two ** B(1200) + B(1), two ** B(2200) + B(1)], // 9.3326e-302 - [-(ten ** B(400) + B(7)), B(3) * ten ** B(400) + B(1)], // -1/3 - [ten ** B(400) + B(1), B(3)], // Infinity - [B(1), ten ** B(400) + B(3)], // 0 (below the double range) - [B(7), two ** B(1074) + B(1)] // subnormal - ]; - for (const [n, d] of cases) { - const value = ExpressionEngine.toFloat(fraction(n, d)); - const reference = rationalToFloat({n, d}); - assert.ok(value === reference || relativeError(value, reference) <= Math.pow(2, -52) || - Math.abs(value - reference) <= Number.MIN_VALUE, n.toString().slice(0, 12) + '... / ' + d.toString().slice(0, 12) + '...: ' + value + ' vs ' + reference); - } - // random non-reducing pairs with up to 2000 bits, quotients across the double range - const g = generator(3); - for (let i = 0; i < 500; i++) { - const n = g.big(1 + g.int(2000)), d = g.big(1 + g.int(2000)); - const value = ExpressionEngine.toFloat(fraction(n, d)); - const reference = rationalToFloat({n, d}); - assert.ok(value === reference || relativeError(value, reference) <= Math.pow(2, -52) || - Math.abs(value - reference) <= Number.MIN_VALUE, value + ' vs ' + reference); - } +test('toFloat of small Fractions is the division of their numbers', () => { + assert.equal(ExpressionEngine.toFloat(N('1/3')), 1 / 3); + assert.equal(ExpressionEngine.toFloat(N('-7/2')), -3.5); + assert.equal(ExpressionEngine.toFloat(N('878291371642703/3279115770832143')), 878291371642703 / 3279115770832143); }); test('the JSON form written by 0.1.17 (numbers) is revived exactly', () => { - const reviver = new ExpressionEngine().getJsonReviver(); const back = JSON.parse('{"a": {"mathjs": "Fraction", "n": -1, "d": 3}, "b": {"mathjs": "Fraction", "n": 878291371642703, "d": 3279115770832143}}', reviver); assertFraction(back.a, '-1/3'); assertFraction(back.b, '878291371642703/3279115770832143'); @@ -214,36 +176,68 @@ test('eval with asNumber: a Unit result is rejected', () => { } }); -// Behaviour changes of Math.js for user expressions (Math.js HISTORY.md), asserted -// for both versions. Changing the meaning of accepted expressions is a decision of -// the product owner; the upgrade PR lists them. -test('[version difference] implicit multiplication after a division with a negative constant numerator (Math.js 11)', () => { - assertFraction(ExpressionEngine.eval('-1 / 2 x', true, {x: 2}), legacy ? '-1/4' : '-1'); - // unchanged - assertFraction(ExpressionEngine.eval('1 / 2 x', true, {x: 2}), '1'); - assertFraction(ExpressionEngine.eval('-1 / (2 x)', true, {x: 2}), '-1/4'); -}); - -test('[version difference] element-wise sqrt, exp, sin of matrices (removed in Math.js 11)', () => { - if (legacy) { - assert.deepEqual(ExpressionEngine.eval('sqrt([4, 9])', false).toArray(), [2, 3]); - } else { - assert.throws(() => ExpressionEngine.eval('sqrt([4, 9])', false)); - } - assert.deepEqual(ExpressionEngine.eval('map([4, 9], sqrt)', false).toArray(), [2, 3]); -}); +// Behaviour changes of Math.js 10-15 for user expressions (Math.js HISTORY.md, +// each reproduced): [expression, scope, 0.1.17 (Math.js 9.5.0), current], values +// of eval(expr, false), 'error' when it throws. Changing the meaning of accepted +// expressions is a decision of the product owner (docs/MATHJS_UPGRADE.md); these +// tests make every known difference explicit. +const show = v => (v && v.isMatrix ? 'matrix ' + JSON.stringify(v.toArray()) : (Array.isArray(v) ? 'array ' + JSON.stringify(v) : String(v))); +const DIFFERENCES = [ + // 15.0: unary percent decoupled from modulus; `%` followed by a term is modulus + ['100 + 3% + 1', {}, '104', '100'], + ['100 * 3% + 1', {}, '4', '0'], + ['200 * 50% - 1', {}, '99', '0'], + ['x% + 1', {x: 2}, '1.02', '0'], + // 14.0: mod has the precedence of * and / (was higher) + ['10 / 4 mod 3', {}, '10', '2.5'], + ['10 * 4 mod 3', {}, '10', '1'], + ['10 / 4 % 3', {}, '10', '2.5'], + // 11.0: implicit multiplication after a division of constants with a sign + ['-1 / 2 x', {x: 2}, '-0.25', '-1'], + ['+1 / 2 x', {x: 2}, '0.25', '1'], + ['-1 / 2 pi', {}, String(-1 / (2 * Math.PI)), String(-Math.PI / 2)], + // 13.0: comparisons with absTol 1e-15 and relTol 1e-12 (was epsilon 1e-12 relative, 2.2e-16 absolute) + ['x > 0', {x: 1e-15}, 'true', 'false'], + ['x == 0', {x: 5e-16}, 'false', 'true'], + ['compare(x, 0)', {x: 1e-15}, '1', '0'], + ['abs(x - 0.3) < 1e-15', {x: 0.1 + 0.2}, 'true', 'false'], + ['isZero(1e-15)', {}, 'false', 'true'], + ['isPositive(1e-15)', {}, 'true', 'false'], + // 15.1: isInteger with tolerance + ['isInteger(2.00000000000001) ? 100 : 0', {}, '0', '100'], + ['isInteger(0.1 * 3 * 10)', {}, 'false', 'true'], + // 11.11.1: mod round-off + ['0.3 mod 0.1', {}, '0.09999999999999998', '-5.551115123125783e-17'], + // 12.1: mod with a negative divisor + ['10 mod -3', {}, 'error', '-2'], + // 10.2: exact combinations + ['combinations(60, 30)', {}, '118264581564861420', '118264581564861460'], + // 11.11.1: print in the parser uses one-based indices + ['print("$1 $2", [10, 20])', {}, '20 $2', '10 20'], + // 11.0: typeOf of a function in lower case + ['typeOf(f)', {f: function () {}}, 'Function', 'function'], + // 11.0: element-wise functions of matrices removed + ['sqrt([4, 9])', {}, 'matrix [2,3]', 'error'], + // 15.1.1: empty true-expression of a conditional + ['x ? : 1', {x: 2}, '1', 'error'], + // 15.0: size returns an array, subsets keep the shape of the index + ['size([1, 2, 3])', {}, 'matrix [3]', 'array [3]'], + ['[[1, 2], [3, 4]][1, :]', {}, 'matrix [[1,2]]', 'matrix [1,2]'], + // 13.0.3: US liquid volume units + ['number(1 gallon, "liter")', {}, '3.785412', '3.785411784'], + // new syntax (14.8) + ['a ?? 1', {a: null}, 'error', '1'] +]; -test('[version difference] mod with a negative divisor, empty true branch of ?:, size(), new operators', () => { - if (legacy) { - assert.throws(() => ExpressionEngine.eval('10 mod -3', false), /negative divisor/); - assert.equal(ExpressionEngine.eval('1 ? : 2', false), 1); - assert.equal(ExpressionEngine.eval('size([1, 2, 3])', false).isMatrix, true); - assert.equal(ExpressionEngine.validate('a ?? 1'), false); - } else { - assert.equal(ExpressionEngine.eval('10 mod -3', false), -2); - assert.throws(() => ExpressionEngine.eval('1 ? : 2', false), SyntaxError); - assert.deepEqual(ExpressionEngine.eval('size([1, 2, 3])', false), [3]); - assert.equal(ExpressionEngine.eval('a ?? 1', false, {a: null}), 1); +test('[version difference] Math.js 10-15 behaviour changes of user expressions', () => { + for (const [expr, scope, before, after] of DIFFERENCES) { + let actual; + try { + actual = show(ExpressionEngine.eval(expr, false, Object.assign({}, scope))); + } catch (e) { + actual = 'error'; + } + assert.equal(actual, legacy ? before : after, expr); } }); @@ -253,3 +247,10 @@ test('[version difference] integers beyond 2^53 in numeric strings', () => { // expressions are still evaluated with JS numbers assert.equal(ExpressionEngine.eval('2^53 + 1', false), 9007199254740992); }); + +test('unchanged: percent of a single operand, mod alone, implicit multiplication without a sign, rounding', () => { + for (const [expr, value] of [['100 + 3%', 103], ['3% * 100', 3], ['50% * 2', 1], ['7 % 3', 1], ['1 / 2 x', 1], ['2 x', 4], + ['0.1 + 0.2 == 0.3', true], ['round(1.005, 2)', 1.01], ['round(-2.5)', -3], ['floor(2.9999999999999996)', 3]]) { + assert.equal(ExpressionEngine.eval(expr, false, {x: 2}), value, expr); + } +}); From b70e2eba1ad49cb03503954f7549900847dd1b1c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 15:07:02 +0000 Subject: [PATCH 4/6] [docs] describe how stored jobs of another Math.js version are resumed sd-computations (fix/job-resume-semantics) records the expression semantics in every job execution and continues a stopped job only under the semantics it was started with; the proposal for old diagrams refers to it instead of an unimplemented marker. --- docs/MATHJS_UPGRADE.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/MATHJS_UPGRADE.md b/docs/MATHJS_UPGRADE.md index e5d96ed..13d4ca4 100644 --- a/docs/MATHJS_UPGRADE.md +++ b/docs/MATHJS_UPGRADE.md @@ -51,8 +51,12 @@ which change with sensitivity analyses and random draws. the new field below is a **legacy** file. - New field written on save, e.g. `data.expressionSemantics: "mathjs-15"` (a name of the expression semantics, not of the app version, so that a later - Math.js upgrade can add another one). Jobs in the job repository store the - semantics of their data model together with the job instance. + Math.js upgrade can add another one). +- Jobs: sd-computations stores the semantics of the engine in every job + execution (`expressionSemantics: "mathjs-"`; records without it are + Math.js 9) and continues a stopped job only under the semantics it was + started with; otherwise the old job is kept and a new one starts from the + beginning (sd-computations `fix/job-resume-semantics`). ### 2. Classification on opening a legacy file (before any recomputation) @@ -108,8 +112,9 @@ SilverDecisions x.y), not recomputed, until the user decides: Accepting writes `expressionSemantics` (and e.g. `migratedFrom: "1.2.1"`) into the diagram; the next save stores it and later openings do not warn. Without a save the warning appears again (the file is still legacy). Jobs: results of -jobs stored by 0.1.x are data and are shown as they are; a re-run uses the -semantics of the current data model. +jobs completed by 0.1.x are data and are shown as they are; a re-run uses the +semantics of the current data model; a job stopped by 0.1.x is not continued +(section 1). ### Costs and open points From d1e35581bbdd4b4afe579414b59bbba6aa1dabc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 19:47:48 +0000 Subject: [PATCH 5/6] [docs] describe the expression semantics profile of stored jobs sd-computations (fix/job-resume-semantics, 52a7ab5) now records a profile of the Math.js version and configuration instead of the Math.js major; expressions that change the shared configuration are listed as a decision. --- docs/MATHJS_UPGRADE.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/MATHJS_UPGRADE.md b/docs/MATHJS_UPGRADE.md index 13d4ca4..f0a00b7 100644 --- a/docs/MATHJS_UPGRADE.md +++ b/docs/MATHJS_UPGRADE.md @@ -52,11 +52,18 @@ which change with sensitivity analyses and random draws. - New field written on save, e.g. `data.expressionSemantics: "mathjs-15"` (a name of the expression semantics, not of the app version, so that a later Math.js upgrade can add another one). -- Jobs: sd-computations stores the semantics of the engine in every job - execution (`expressionSemantics: "mathjs-"`; records without it are - Math.js 9) and continues a stopped job only under the semantics it was - started with; otherwise the old job is kept and a new one starts from the - beginning (sd-computations `fix/job-resume-semantics`). +- Jobs: sd-computations (`fix/job-resume-semantics`) stores an expression + semantics profile in every job execution: profile version, full Math.js + version and the whole Math.js configuration (records without it: 0.1.17 with + its defaults). A stopped job is continued only under the same profile; + otherwise the old job is kept and a new one starts from the beginning, and a + worker with another profile than the page does not compute the page's jobs. + The profile is read from the shared Math.js instance, so a decision on the + comparison tolerances (`absTol`) changes it, and so does `config(...)` in an + expression: such a diagram cannot run restartable analyses in the + SilverDecisions worker (the worker evaluated its code, the page did not) until + expressions either may not change the configuration or each evaluation gets a + configuration owned by the diagram (a decision, see below). ### 2. Classification on opening a legacy file (before any recomputation) @@ -131,5 +138,9 @@ semantics of the current data model; a job stopped by 0.1.x is not continued or the Math.js 13+ tolerances. 3. Handling of legacy diagrams (sections 1-5) or an alternative, before a SilverDecisions release with Math.js 15. -4. JSON form of Fractions with string `n` / `d` in the job repository (read +4. Expressions that change the shared Math.js instance (`config(...)`, + `import`, `createUnit`): allowed as now (their jobs in the worker are refused), + rejected, or isolated per diagram; this decides whether the expression + semantics profile of jobs can be guaranteed for every diagram. +5. JSON form of Fractions with string `n` / `d` in the job repository (read back from 0.1.17 records works, the reverse does not). From f6d361bf339424008818eef73d0f33ee546cd102 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 11:48:03 +0000 Subject: [PATCH 6/6] [docs] mark the job resume guard as a draft that waits for a decision fix/job-resume-semantics is not ready to be merged on its own: it refuses analyses of diagrams that call config(...) also with 0.1.17, and its profile does not cover createUnit / import. --- docs/MATHJS_UPGRADE.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/MATHJS_UPGRADE.md b/docs/MATHJS_UPGRADE.md index f0a00b7..8418070 100644 --- a/docs/MATHJS_UPGRADE.md +++ b/docs/MATHJS_UPGRADE.md @@ -52,7 +52,8 @@ which change with sensitivity analyses and random draws. - New field written on save, e.g. `data.expressionSemantics: "mathjs-15"` (a name of the expression semantics, not of the app version, so that a later Math.js upgrade can add another one). -- Jobs: sd-computations (`fix/job-resume-semantics`) stores an expression +- Jobs (sd-computations `fix/job-resume-semantics`, a draft, not ready to be + merged on its own, see the decision on expressions below): it stores an expression semantics profile in every job execution: profile version, full Math.js version and the whole Math.js configuration (records without it: 0.1.17 with its defaults). A stopped job is continued only under the same profile; @@ -63,7 +64,9 @@ which change with sensitivity analyses and random draws. expression: such a diagram cannot run restartable analyses in the SilverDecisions worker (the worker evaluated its code, the page did not) until expressions either may not change the configuration or each evaluation gets a - configuration owned by the diagram (a decision, see below). + configuration owned by the diagram (a decision, see below). This refusal + applies with 0.1.17 too. The profile does not cover the rest of the mutable + Math.js state (`createUnit`, `import`). ### 2. Classification on opening a legacy file (before any recomputation)