Repository navigation
Add an engine-free runtime for compiled JavaScript (@cortex-js/compute-engine/runtime) - #408
enumeratio wants to merge 6 commits into
Conversation
…ute-engine/runtime, built from the factory the engine's own _SYS uses; seeded frames, random source, iteration limit and deadline are options; add runtimeVersion to compilation results; move the static helpers into modules with no engine import; add a three-way seeded test and a bundle test. Fix cortex-js#372
# Conflicts: # src/compute-engine/compilation/javascript-target.ts
arnog
left a comment
There was a problem hiding this comment.
Thanks, this meets the five conditions from #372. The move is clean: an AST comparison found the 846 moved declarations and all 209 helpers unchanged. The seeded runs agree three ways. Requested changes:
- Rebase onto current
main. Since your last merge ofmain, ~175 lines changed inside the code this PR moves: 30 changed helpers (pow,cpow, the complex trig and inverse trig helpers,csqrt,cexp,cln, …), 4 new ones (sinpi,cospi,tanpi,cexppi), andchopKernelDustwas removed. Git will not prompt you to port these into the newjavascript-runtime.ts. If the four new helpers are missed, the type check still passes, but compiled code throws at run time. Please extract the helpers again from the currentjavascript-target.ts. load()runs the whole preamble on every call.run()runs the definitions that do not read_only once, butload()rebuilds constant lists and resets the memos on each call. We measured 6× slower forAt(L, Floor(x))and 2.6× forMax(L) + xwith a 1,000-elementL. Per-pixel sampling is the use case, so please store the once-only and per-call parts of the preamble on the result, and haveload()build the two-stage function.- Narrow the published type.
JavaScriptRuntimeshould expose onlyload,frame,iterationLimit,deadlineandruntimeVersion, with the helper table opaque. The helpers must be free to change in any release. - "No engine module" test: please use an allow-list of input paths (or an
import/no-restricted-pathszone in.eslintrc.cjs) instead of a list of forbidden paths. - Smaller items:
- The type of
frameshould accept{ seed, next }. rt.iterationLimitshould returnInfinityfor a setting of 0 or less, asce.iterationLimitdoes.- The smoke test should check that the version string was replaced by the build, not only that the two sides are equal.
- Please document that
CapabilityDeniedErrormust be tested withe.name, because it is a different class in each bundle. - Optionally,
load()could reject code that has noruntimeVersion.
- The type of
…ers into javascript-runtime.ts (30 changed, 4 new, chopKernelDust removed); move the negative-base real-power branch decision to numerics/real-power.ts so the runtime stays engine-free
…e function run() uses (preambleOnce/preamblePerCall/callCode on the result), the published JavaScriptRuntime type exposes only load/frame/iterationLimit/deadline/runtimeVersion, iterationLimit reads Infinity for 0 or less, load() requires runtimeVersion, the bundle test uses an allow-list, the smoke test checks the version was replaced; CHANGELOG entry back under Unreleased. Fix cortex-js#372
|
What changed, per item:
The CHANGELOG entry stays under Unreleased and is updated. Size is in the description. |
arnog
left a comment
There was a problem hiding this comment.
Thanks — all five items are done, and we verified them: the re-extraction matches current main exactly (851 declarations and 213 helpers identical), load() is now as fast as run(), the type is narrow, and the bundle test uses an allow-list. Typecheck, madge and 226 related suites pass. Two places remain where loaded code disagrees with run():
- Input conversions. load() calls the generated function directly and skips the entry conversions that run() applies. With
zdeclared complex,z^2 + zgives 6 fromrun({z: 2})but{re: NaN, im: NaN}fromload(result)({z: 2}). AFloat64Arrayfor a list variable gives 6 from run() and null from load(). Please store the engine-independent part of the conversion plan with the result and apply it in load(). - Reconstruction precision for a negative base. The /runtime bundle has its own BigDecimal (precision 50), so realPowerReconstructionDigits() always picks 17 there, while a machine-precision engine picks 15. With
ce.precision = 'machine',(-2)^xat x = 33.3333333333333 gives 10822639409.68 from run() and NaN from the bundled runtime. Please carry the digit count with the result (the Python target already bakes it into_ce_pow), or take it as a runtime option. docs/COMPILATION-MODEL.md should list it with tolerance and angular unit as fixed at compile time. Tests in jest miss this because jest shares one BigDecimal between engine and runtime; a check in the bundle smoke test would catch it.
Nits: rationalize and reducedRational are now unused in arithmetic-power.ts; and the getter and setter of JavaScriptRuntime.frame have different types, which needs TypeScript 5.1+. Please document that or use one type.
…the input conversions run() does (entryPlan on the result), the digits a negative base's float exponent is read to are fixed at compile time (reconstructionDigits) so the runtime's own precision no longer decides, the frame is set with setFrame(), drop the unused rationalize and reducedRational imports; the smoke test checks both on the built bundles. Fix cortex-js#372
# Conflicts: # CHANGELOG.md
|
Thanks. All three are fixed, with tests.
|
|
Thanks — the second review's three items were all fixed, and we verified them, including in separately built bundles. Rather than ask for another merge with
This will be in the next release. |
Option (1) of #372, with the five conditions from the discussion.
createJavaScriptRuntime(options)returns the_SYSbundle thatcompile()builds forrun(), with no engine behind it. The code of aJavaScriptTargetresult reads only_SYSand_, so it can be stored and run on a page, in a worker or on a server:runtime.load(result)returns the function. The runtime type exposes onlyload,frame,iterationLimit,deadlineandruntimeVersion; the helper table is not part of it and may change in any release.1. Seeded randomness. The runtime includes
frameDrawandfoldSeed, soWithRandomSeedinside compiled code gives the interpreter's values.frametakes an outer frame ({ seedLo, seedHi, next }or{ seed, next }, also when set on the runtime), andruntime.frame.nextis the advanced counter after the call.randomis used only for draws outside a frame and for the integrals' Monte-Carlo samples (live inside a frame too);random: nulldenies, and a draw then throws the sameCapabilityDeniedError('entropy')as the engine. The class differs in each bundle, so teste.name, notinstanceof(documented).2. Limits.
iterationLimit(default 1024, the engine's; 0 or less means no cap, andrt.iterationLimitreadsInfinitythen, asce.iterationLimit) anddeadline(aDate.now()timestamp, off when unset) are options, and settable on the runtime. The lazy-stream helpers and the shuffle/choice loops read them at call time. Thefunctions/importslimit is documented indocs/COMPILATION-MODEL.mdand the CHANGELOG: those functions are copied as source, so a closure loses its scope and a named function must exist where the code runs.3. Version.
CompilationResult.runtimeVersionandruntime.runtimeVersion(also exported asruntimeVersion) are the package version.load()throws on a mismatch, and on code with noruntimeVersion. No helper-set hash.4. One implementation. The static helpers moved out of
compilation/javascript-target.tsintocompilation/javascript-runtime.ts(and the jet arithmetic intocompilation/jet-helpers.ts), with no engine import. The engine'smakeSysHelpersis nowmakeSysHelpers(source), called with aRuntimeSourcebuilt fromce._liveRandom/ce._random,ce._randomFrame,ce.iterationLimitandce._deadlineFrame; the runtime builds one from its options.nextFrameDrawandwithSeedFrame(innumerics/random.ts) are shared byce._random(),withRandomSeedFrameand the runtime.run.SYSis unchanged. The entry point is built asesm-min/runtime.jsandumd-min/runtime.cjs, inexports, and in the nodenext smoke test; a built-bundle smoke test (npm run test:js-runtime) runs in the production build.Repeated calls.
load()builds the same two-stage function asrun(): the definitions that read nothing per call (a constant list, a memo) are evaluated once, the rest on every call. The result carries the split aspreambleOnce,preamblePerCalland, for a lambda,callCode(preambleandcodeare unchanged); stored code without them runs its preamble per call as before. A test counts the constructions of a 1,000-element list over many calls: once withload(), once per call for a legacy result.5. Size and tests.
dist/esm-min/runtime.jsdist/umd-min/runtime.cjsIt still pulls in the numerics the helpers call:
@arnog/colors(~38 KB minified),numeric-complex(~36 KB),special-functions(~25 KB),BigDecimaland its transcendentals (~36 KB together) andlerch-phi(~13 KB). It has no boxed expression, library, LaTeX or compiler code. Splitting the colour and special-function helpers behind their own entry would shrink it; not done here.test/compute-engine/js-runtime.test.tsruns the same seeded programs (Random, several draws,RandomShuffle,RandomChoice,RandomSample, a nested frame) interpreted, withrun()and as stored.codeon the runtime, with numeric and string seeds, and checks the outer-frame round trip,random: null, the limits, the version check, and stored expressions and lambdas. A bundle test buildssrc/runtime.tswith esbuild, asserts that every input is on an allow-list of runtime and numerics paths, and runs the bundle. The built-bundle smoke test also checks that the build replaced the version placeholder.Fix #372