Skip to content

Keep numeric.js's functions on me.math in the js-compat drop-in - #87

Open
dqnykamp wants to merge 1 commit into
Doenet:mainfrom
dqnykamp:restore-numeric-js-compat
Open

Keep numeric.js's functions on me.math in the js-compat drop-in#87
dqnykamp wants to merge 1 commit into
Doenet:mainfrom
dqnykamp:restore-numeric-js-compat

Conversation

@dqnykamp

@dqnykamp dqnykamp commented Aug 7, 2026

Copy link
Copy Markdown
Member

Companion to #86, which fixes the same underlying problem on the 2.x branch. This one is about the drop-in on main.

me.math lost numeric's functions

The original library built me.math as math.js plus numeric.js:

math.import(numeric, { wrap: true, silent: true });

packages/math-expressions-js-compat/lib/mathjs.ts re-exports a bare math.js instance, so every name numeric contributed and math.js has no equivalent of is simply gone. DoenetML's <odeSystem> integrates with me.math.dopri, which is one of them — it is the only numeric-only name DoenetML reaches for, but it is load-bearing: adopting this package as published would turn a working ODE into dopri is not a function.

So this imports numeric here too. silent skips the names math.js already defines (add, sqrt, round, …), leaving math.js's own implementations in place — the same arrangement the 2.x line has always shipped.

…and numeric needs a global to be usable

numeric.js builds most of its helpers at load time with the Function constructor, and the generated bodies reference a bare numeric:

if(typeof _s === "undefined") _s = numeric.dim(x);

Functions made with Function(...) are evaluated in global scope, so that reference resolves only if numeric is a property of the global object. numeric.js puts it there itself — but through a Node-ism:

if (typeof global !== "undefined") { global.numeric = numeric; }

A browser main thread and a web worker both have no global, so the assignment is skipped and every generated helper throws ReferenceError: numeric is not defined the first time it is called. dopri reaches those helpers immediately, which is why the bug is invisible to a Node test suite and fatal in the browser. Publishing it from lib/mathjs.ts covers every runtime.

The assignment is unconditional rather than guarded on globalThis.numeric === undefined. On a page holding an element whose id is numeric, the named-element global makes the slot look occupied while still being useless to the generated code — which is what happens in DoenetML, whose virtual keyboard has a numeric button, and it turns the error into the more puzzling dim is not a function.

Test

spec/quick_mathjs-numeric.spec.ts deletes global before the first import of lib/mathjs — reproducing the shape a browser sees — then checks that the module registered numeric itself, and integrates x' = x from x(0) = 1 to confirm dopri is both present and callable.

The js-compat suite is not green on main (hence the || true in CI), so here is the before/after rather than a bare pass:

test files tests
main 26 failed, 1 passed, 2 skipped 1453 failed, 4593 passed
this branch 26 failed, 2 passed, 2 skipped 1453 failed, 4594 passed

Exactly the one new test, no new failures. prettier --check is clean on the changed files, and tsc reports nothing new for them beyond the pre-existing Cannot find name 'describe' that every spec in the package already has.

numeric@1.2.6 is added to the package's dependencies; it was already resolved in the lockfile, so that diff is two lines.

🤖 Generated with Claude Code

`me.math` in the original library was math.js *plus* numeric.js:

    math.import(numeric, { wrap: true, silent: true });

The drop-in re-exports a bare math.js instance, so every name numeric
contributed and math.js has no equivalent of is gone. DoenetML's
`<odeSystem>` integrates with `me.math.dopri`, which is one of them — so
moving to this package turns a working ODE into `dopri is not a
function`. Import numeric here too; `silent` keeps math.js's own
implementations for the names both define.

Registering numeric on the global object is part of making it usable.
numeric.js builds most of its helpers at load time with the `Function`
constructor, and the generated bodies reference a bare `numeric` — e.g.
`if(typeof _s === "undefined") _s = numeric.dim(x);`. Functions made that
way are evaluated in global scope, so that reference resolves only if
`numeric` is a property of the global object. numeric.js puts it there
itself, but only through Node's `global`, which a browser and a web
worker both lack; there, every generated helper throws `ReferenceError:
numeric is not defined` on first call, `dopri` included.

The assignment is unconditional rather than guarded on
`globalThis.numeric === undefined`: on a page holding an element whose id
is `numeric`, the named-element global makes the slot look occupied while
still being useless to the generated code.

`spec/quick_mathjs-numeric.spec.ts` deletes `global` before the first
import of `lib/mathjs`, reproducing the shape a browser sees, then
integrates x' = x from x(0) = 1 and checks it lands on e.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant