From 6500ad41477455ea043e48b71f035d89a02399b3 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Fri, 4 Sep 2026 16:33:36 -0700 Subject: [PATCH 1/7] Fix rust host-triple detection with rustc 1.98 rustc 1.98 adds the OpenEmbedded targets (x86_64-oe-linux-gnu and friends) to its target list, so an x86_64-pc-linux-gnu host now has two candidate rust targets and no narrowing step chose between them: the host vendor "pc" matches neither "unknown" nor "oe", and configure died with "Don't know how to translate x86_64-pc-linux-gnu for rustc". Prefer the generic "unknown" vendor when the vendor match is inconclusive. --- build/moz.configure/rust.configure | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build/moz.configure/rust.configure b/build/moz.configure/rust.configure index 6c1c891817133..3e95528198282 100644 --- a/build/moz.configure/rust.configure +++ b/build/moz.configure/rust.configure @@ -417,6 +417,13 @@ def detect_rustc_target( if len(narrowed) == 1: return narrowed[0].rust_target + # Failing that, prefer the generic "unknown" vendor over a + # vendor-specific target (rustc 1.98 added x86_64-oe-linux-gnu beside + # x86_64-unknown-linux-gnu, and the host vendor is "pc"). + narrowed = [c for c in candidates if c.target.vendor == "unknown"] + if len(narrowed) == 1: + return narrowed[0].rust_target + return None rustc_target = find_candidate(candidates) From 3bafb0b81e23b82f7254d5475cb1c7afdb29bca4 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Fri, 4 Sep 2026 16:01:06 -0700 Subject: [PATCH 2/7] Introduce NightMonkey: an ahead-of-time JS-to-Wasm compiler. **NightMonkey** is an ahead-of-time JS-to-Wasm compilation tier built inside SpiderMonkey. NIGHT expands to *Nonlocal Inference with Guiding Heuristics for Types*: an optimistic whole-program type analysis guides code generation, with dynamic guards for correctness. (The night monkey, genus *Aotus*, is the only truly nocturnal monkey: it does its work in the night, before the program runs during the day.) Each JS function's bytecode is compiled to a WebAssembly function that runs alongside the runtime compiled to Wasm. There are two modes of use: - **Snapshot** (the shipping flow): the `nightmonkey` host binary drives Wizer in-process to snapshot the runtime plus loaded user program (or processes an existing Wizer snapshot), reads out JS bytecode and heap objects (such as prototype objects), and rewrites that snapshot with compiled bodies. - **In-process** (the testing flow): the JS shell compiled to Wasm runs under `wasm-jit-runner`, walks its own live heap, compiles the script tree, and injects the bodies into its running instance via runner hostcalls (`--night-inprocess`). A drop-in shell for jit-tests. NightMonkey has a two-part structure: an *optimistic static type analysis* and a *guard-based codegen backend*. The idea is that we: 1. "Predict" types statically, using a model of JavaScript semantics that is intentionally optimistic (elides corner-cases). We call this the "likelier-types analysis" (in a nod to the initial version of the analysis, the "likely-types analysis"; this one is a little better). 2. Generate an optimistic Wasm body for a given JS function bytecode body, using those predicted types. 3. Insert dynamic guards checking those assumptions, with fallbacks to a fully generic (but still compiled!) Wasm body. The *key constraint* that NightMonkey adheres to, and attempts to solve: we cannot derive type information, or any other profiling information, by observing a running program. In other words, unlike the standard JIT approach based on the "JIT hypothesis" (that a warmed-up program will reach a steady state with stable types, which we can then specialize for), we must decide any specialization we will do ahead-of-time, based on whatever analysis or heuristics we can come up with. The thing we permit ourselves in return is much more analysis time: unlike a JIT engine, we do not need to compile in milliseconds. NightMonkey performs its analysis using a whole-program, call-sensitive, points-to (heap abstraction) + callgraph analysis, over a lattice that is a hybrid of a Steensgaard (union-find-based) and capped Andersen (points-to-set/membership-based) design. The codegen using the types that come out of this analysis is then a "two-track" approach: there is one optimistic track that adheres to "type contexts" that are maximally optimal, and one fully generic track. (Earlier experiments tried to do more multiversioning, a la Static Basic Block Versioning, but that did not converge well.) As of 2026-09-04, comparing to native IonMonkey and baseline tiers, and against Wasm-hosted interpreter and weval+PBL execution: ```plain bench native-ion nat-baseline wasm-interp weval aot aot/wasm-int aot/weval weval/wasm-int ion/weval ion/aot baseline/aot richards 29205 6489 377 936 11893 31.55 12.71 2.48 31.20 2.46 0.55 deltablue 28179 6870 395 978 6678 16.91 6.83 2.48 28.81 4.22 1.03 crypto 42755 5654 714 949 15696 21.98 16.54 1.33 45.05 2.72 0.36 raytrace 58549 11458 1045 1815 11964 11.45 6.59 1.74 32.26 4.89 0.96 earley-boyer 83262 21153 1510 3982 15088 9.99 3.79 2.64 20.91 5.52 1.40 navier-stokes 43926 8269 1223 2090 24980 20.43 11.95 1.71 21.02 1.76 0.33 splay 29291 23303 5248 6853 10693 2.04 1.56 1.31 4.27 2.74 2.18 regexp 18601 7223 596 766 2484 4.17 3.24 1.29 24.28 7.49 2.91 pdfjs 95804 40738 4116 6185 24743 6.01 4.00 1.50 15.49 3.87 1.65 mandreel 73940 11619 865 1269 19545 22.60 15.40 1.47 58.27 3.78 0.59 code-load 70224 69259 37108 37005 37271 1.00 1.01 1.00 1.90 1.88 1.86 box2d 99321 22370 1896 4135 25999 13.71 6.29 2.18 24.02 3.82 0.86 react-bench 0.631 1.415 15.026 10.421 2.862 5.25 3.64 1.44 16.52 4.54 2.02 geomean 49233 14111 1527 2569 14247 8.93 5.37 1.66 18.94 3.53 1.05 (octane = Score higher-better; react-bench = ms/render lower-better; best-of-3, taskset -c 1) (ratio cols = speedup of A over B, direction-corrected for react-bench; geomean row: lane cols over octane scores only, ratio cols over all benches) ``` We can conclude that NightMonkey is ~9x faster than the Wasm interpreter on average, or ~5x faster than weval+PBL. It is nearly on par with the native baseline compiler, and within ~3.5x of the IonMonkey optimized native-code ceiling (while running within a Wasm engine). On benchmarks where type-based specialization works especially well, NightMonkey comes within ~2.5x (e.g. Richards) of native Ion. This work has been done over the past ~6 months with the use of LLM-assisted code generation (mainly Claude Opus and Fable) but with careful line-by-line review and obsessive review of analysis results and generated code. I've read through the whole implementation, checked it to the best of my ability and will continue to do so. It is "a lot" but it does pass jit-tests cleanly. This commit message and the README are fully human-authored. --- .cargo/config.toml.in | 25 +- .prettierignore | 1 + Cargo.lock | 91 +- Cargo.toml | 1 + build/workspace-hack/Cargo.toml | 1 + config/check_spidermonkey_style.py | 5 + eslint-ignores.config.mjs | 1 + js/moz.configure | 69 + js/public/NightMonkey.h | 31 + js/src/builtin/RegExp.cpp | 21 +- js/src/builtin/RegExp.h | 31 + js/src/builtin/String.cpp | 11 +- js/src/builtin/String.h | 2 + js/src/gc/Nursery.h | 2 +- js/src/irregexp/RegExpAPI.cpp | 16 + js/src/irregexp/RegExpAPI.h | 34 + js/src/jit-test/tests/arguments/bug1227287.js | 1 + js/src/jit-test/tests/arguments/bug1423937.js | 2 +- js/src/jit-test/tests/arguments/bug1827073.js | 2 +- .../function_dot_caller_restrictions.js | 1 + .../tests/arrow-functions/column-number.js | 5 +- js/src/jit-test/tests/asm.js/testBug878520.js | 1 + .../jit-test/tests/asm.js/testStackWalking.js | 1 + js/src/jit-test/tests/atomics/pause-multi.js | 2 +- js/src/jit-test/tests/atomics/pause-single.js | 2 +- .../jit-test/tests/auto-regress/bug1147907.js | 1 + .../tests/auto-regress/bug1448582-1.js | 1 + .../tests/auto-regress/bug1448582-2.js | 1 + .../tests/auto-regress/bug1448582-3.js | 1 + .../tests/auto-regress/bug1448582-4.js | 1 + .../tests/auto-regress/bug1448582-5.js | 1 + .../tests/auto-regress/bug1448582-6.js | 1 + .../jit-test/tests/auto-regress/bug1916581.js | 1 + .../jit-test/tests/auto-regress/bug1928407.js | 1 + js/src/jit-test/tests/baseline/bug836742.js | 1 + .../tests/baseline/metadata-hook-on-stack.js | 1 + js/src/jit-test/tests/basic/allow-relazify.js | 2 +- js/src/jit-test/tests/basic/bug-1649234-2.js | 2 +- js/src/jit-test/tests/basic/bug1015339.js | 1 + js/src/jit-test/tests/basic/bug1122581.js | 1 + js/src/jit-test/tests/basic/bug1293575.js | 1 + js/src/jit-test/tests/basic/bug1731540.js | 2 +- js/src/jit-test/tests/basic/bug1883828.js | 1 + js/src/jit-test/tests/basic/bug592927.js | 1 + js/src/jit-test/tests/basic/bug709634.js | 1 + js/src/jit-test/tests/basic/bug827104.js | 1 + js/src/jit-test/tests/basic/bug839420.js | 1 + js/src/jit-test/tests/basic/bug951632.js | 1 + .../jit-test/tests/basic/decompile-script.js | 1 + js/src/jit-test/tests/basic/eif-generator.js | 1 + .../tests/basic/exception-column-number.js | 2 +- .../tests/basic/expression-autopsy.js | 2 +- js/src/jit-test/tests/basic/functionnames.js | 2 +- .../tests/basic/iterable-error-messages.js | 2 +- .../jit-test/tests/basic/json-parse-errors.js | 2 +- .../basic/metadata-hook-regexp-result.js | 1 + js/src/jit-test/tests/basic/metadata-hook.js | 1 + .../tests/basic/prop-access-error-message.js | 2 +- .../property-error-message-fix-disabled.js | 2 +- .../tests/basic/property-error-message-fix.js | 2 +- .../jit-test/tests/basic/runOnceClosures.js | 1 + .../jit-test/tests/basic/segmenter-atomref.js | 1 + js/src/jit-test/tests/basic/testBug552248.js | 1 + .../jit-test/tests/basic/testBug663789-2.js | 1 + .../jit-test/tests/basic/testDynamicLookup.js | 1 + .../tests/basic/testEvalInFrameEdgeCase.js | 1 + js/src/jit-test/tests/basic/testPaths.js | 1 + .../basic/throw-exception-stack-location.js | 1 + .../tests/basic/throw-exception-stack.js | 1 + .../tests/basic/track-allocation-sites.js | 1 + js/src/jit-test/tests/bug1742592.js | 1 + .../cacheir/atomics-store-non-number-value.js | 1 + .../cacheir/inlinable-native-accessor-7.js | 1 + .../cacheir/inlinable-native-accessor-8.js | 1 + .../object-constructor-metadata-builder.js | 1 + js/src/jit-test/tests/cacheir/rope-char-at.js | 2 + .../class/checkreturn-source-location.js | 3 +- .../resizable-dataview-bytelength-with-sab.js | 1 + .../resizable-dataview-byteoffset-sab.js | 1 + .../resizable-dataview-get-elem-with-sab.js | 1 + .../resizable-dataview-set-elem-with-sab.js | 1 + .../tests/debug/Debugger-findScripts-27.js | 2 +- .../tests/debug/Debugger-findScripts-28.js | 2 +- .../tests/debug/Debugger-findScripts-29.js | 2 +- .../tests/debug/Debugger-findScripts-30.js | 2 +- .../tests/debug/Debugger-findScripts-31.js | 2 +- .../debug/Debugger-findScripts-delazify.js | 2 +- .../tests/debug/Environment-setVariable-16.js | 1 + .../tests/debug/Environment-setVariable-17.js | 1 + .../tests/debug/Environment-setVariable-18.js | 1 + js/src/jit-test/tests/debug/Frame-eval-25.js | 1 + js/src/jit-test/tests/debug/Frame-eval-33.js | 1 + .../jit-test/tests/debug/Frame-eval-stack.js | 1 + .../jit-test/tests/debug/Frame-identity-01.js | 1 + .../jit-test/tests/debug/Frame-identity-02.js | 1 + .../jit-test/tests/debug/Frame-identity-03.js | 1 + .../jit-test/tests/debug/Frame-onStack-02.js | 1 + .../tests/debug/Memory-takeCensus-09.js | 1 + .../Memory-trackingAllocationSites-03.js | 1 + .../debug/Object-getPromiseReactions-07.js | 1 + js/src/jit-test/tests/debug/Source-url-01.js | 1 + js/src/jit-test/tests/debug/Source-url-02.js | 1 + js/src/jit-test/tests/debug/bug-1444604.js | 1 + js/src/jit-test/tests/debug/bug-1477084.js | 1 + js/src/jit-test/tests/debug/bug1109915.js | 1 + js/src/jit-test/tests/debug/bug1109964.js | 1 + js/src/jit-test/tests/debug/bug1188334.js | 1 + js/src/jit-test/tests/debug/bug1302432.js | 1 + js/src/jit-test/tests/debug/bug1304553.js | 1 + js/src/jit-test/tests/debug/bug1385843.js | 1 + js/src/jit-test/tests/debug/bug1417961.js | 1 + .../debug/bug1644699-terminated-generator.js | 2 +- js/src/jit-test/tests/debug/bug1812979.js | 2 +- js/src/jit-test/tests/debug/bug1814020.js | 1 + js/src/jit-test/tests/debug/bug1817933.js | 1 + js/src/jit-test/tests/debug/bug1851135.js | 2 +- .../debug/envChain_frame-eval-relazify.js | 2 +- .../tests/debug/execution-observability-04.js | 1 + .../tests/debug/execution-observability-05.js | 1 + .../debug/onEnterFrame-async-resumption-07.js | 1 + .../tests/debug/onExceptionUnwind-02.js | 1 + .../tests/debug/onExceptionUnwind-03.js | 1 + .../debug/private-methods-eval-in-frame.js | 1 + .../tests/debug/prologueFailure-01.js | 1 + .../tests/debug/prologueFailure-02.js | 1 + .../tests/debug/prologueFailure-03.js | 1 + js/src/jit-test/tests/debug/resumption-03.js | 1 + js/src/jit-test/tests/debug/resumption-05.js | 1 + .../jit-test/tests/environments/bug1966196.js | 2 +- js/src/jit-test/tests/errors/bug1961019.js | 1 + .../tests/errors/capture-stack-jit.js | 2 +- js/src/jit-test/tests/errors/capture-stack.js | 2 +- js/src/jit-test/tests/fields/bug1702420.js | 2 +- .../tests/fields/private-eval-in-frame.js | 1 + js/src/jit-test/tests/for-of/interrupt-1.js | 2 +- js/src/jit-test/tests/for-of/interrupt-2.js | 2 +- js/src/jit-test/tests/for-of/interrupt-3.js | 2 +- js/src/jit-test/tests/function/bug-1751660.js | 1 + .../function/function-displayName-computed.js | 2 +- .../fuses/species-fuse-sharedarraybuffer-1.js | 1 + .../fuses/species-fuse-sharedarraybuffer-2.js | 1 + js/src/jit-test/tests/gc/bug-1603330.js | 2 + js/src/jit-test/tests/gc/bug-1867453.js | 2 + js/src/jit-test/tests/gc/bug-1997896.js | 2 + js/src/jit-test/tests/gc/gcparam.js | 1 + js/src/jit-test/tests/gc/pretenuring.js | 2 + .../tests/gc/symbols-as-weakmap-keys.js | 3 +- .../tests/gc/weakRefs-with-symbol-keys.js | 3 +- js/src/jit-test/tests/ion/bug1001378.js | 1 + js/src/jit-test/tests/ion/bug1005458.js | 1 + js/src/jit-test/tests/ion/bug1077349.js | 1 + js/src/jit-test/tests/ion/bug1299007.js | 1 + js/src/jit-test/tests/ion/bug1510684.js | 1 + js/src/jit-test/tests/ion/bug1791520.js | 1 + js/src/jit-test/tests/ion/bug1877709.js | 1 + js/src/jit-test/tests/ion/bug1921215.js | 2 +- js/src/jit-test/tests/ion/bug1987592.js | 2 +- js/src/jit-test/tests/ion/bug754720.js | 1 + js/src/jit-test/tests/ion/bug758991.js | 1 + js/src/jit-test/tests/ion/bug813784.js | 1 + js/src/jit-test/tests/ion/bug818023.js | 1 + js/src/jit-test/tests/ion/bug824473.js | 1 + js/src/jit-test/tests/ion/bug835178.js | 1 + js/src/jit-test/tests/ion/bug977966.js | 2 +- .../tests/ion/dce-with-rinstructions.js | 2 +- js/src/jit-test/tests/ion/is-constructing.js | 1 + .../tests/ion/recover-atomics-islockfree.js | 2 +- .../tests/ion/recover-int64tobigint.js | 2 +- .../tests/ion/recover-lambdas-bug1133389.js | 1 + js/src/jit-test/tests/ion/recover-lambdas.js | 2 +- js/src/jit-test/tests/ion/recover-objects.js | 2 +- .../ion/test-scalar-replacement-float32.js | 1 + .../tests/jaeger/argumentsOptimize-1.js | 1 + .../jaeger/bug563000/eif-call-typechange.js | 1 + .../tests/jaeger/bug563000/eif-call.js | 1 + .../jaeger/bug563000/eif-getter-newvar.js | 1 + .../jaeger/bug563000/eif-getter-typechange.js | 1 + .../tests/jaeger/bug563000/eif-getter.js | 1 + .../jaeger/bug563000/eif-global-newvar.js | 1 + js/src/jit-test/tests/jaeger/bug710780.js | 1 + js/src/jit-test/tests/jaeger/getter-hook-2.js | 1 + .../tests/jaeger/invokeSessionGuard.js | 1 + .../jit-test/tests/jaeger/loops/hoist-05.js | 1 + js/src/jit-test/tests/latin1/decompiler.js | 1 + js/src/jit-test/tests/modules/bug-1245518.js | 1 + js/src/jit-test/tests/modules/bug-1498980.js | 2 +- js/src/jit-test/tests/modules/bug-1782496.js | 2 +- .../tests/modules/failure-on-resume.js | 1 + .../tests/night/define-elem-over-sparse.js | 14 + .../night/exception-unwinds-lexical-env.js | 48 + .../tests/night/function-name-lazy-resolve.js | 8 + .../night/gc-callback-keeps-cache-purge.js | 11 + .../tests/night/regexp-exec-lastindex-read.js | 14 + js/src/jit-test/tests/profiler/bug2002982.js | 2 + .../tests/profiler/interpreter-stacks.js | 2 +- .../newpromisecapability-error-message.js | 2 +- js/src/jit-test/tests/realms/bug1518821.js | 1 + .../tests/realms/scripted-caller-global.js | 1 + .../math-fdlibm-sincostan.js | 1 + .../saved-stacks/1438121-async-function.js | 1 + .../jit-test/tests/saved-stacks/asm-frames.js | 7 +- .../tests/saved-stacks/async-implicit.js | 1 + .../tests/saved-stacks/async-livecache.js | 1 + .../saved-stacks/async-max-frame-count.js | 1 + js/src/jit-test/tests/saved-stacks/async.js | 1 + .../saved-stacks/bug-1505387-dbg-eval-ion.js | 2 +- .../tests/saved-stacks/bug-1509420.js | 1 + .../tests/saved-stacks/bug-1744495.js | 2 +- .../jit-test/tests/saved-stacks/bug1907801.js | 1 + js/src/jit-test/tests/saved-stacks/evals.js | 1 + .../saved-stacks/function-display-name.js | 1 + .../tests/saved-stacks/gc-frame-cache.js | 1 + .../jit-test/tests/saved-stacks/generators.js | 1 + js/src/jit-test/tests/saved-stacks/get-set.js | 1 + .../getters-on-invalid-objects.js | 1 + .../tests/saved-stacks/max-frame-count.js | 1 + .../tests/saved-stacks/native-calls.js | 1 + .../tests/saved-stacks/proxy-handlers.js | 1 + .../tests/saved-stacks/self-hosted.js | 1 + .../saved-stacks/shared-parent-frames.js | 1 + .../stringify-with-self-hosted.js | 1 + .../method-called-on-incompatible.js | 1 + .../jit-test/tests/self-hosting/relazify.js | 3 +- .../self-test/baselineCompile-Bug1444894.js | 1 + .../sharedbuf/growable-sab-over-mailbox.js | 1 + .../growable-shared-array-buffers.js | 1 + .../tests/structured-clone/saved-stack.js | 1 + .../tests/structured-clone/tenuring.js | 2 + .../tests/typedarray/arraybuffer-pin.js | 1 + .../tests/typedarray/arraybuffer-transfer.js | 2 +- ...nstruct-with-growable-sharedarraybuffer.js | 1 + .../growable-sharedarraybuffer-bytelength.js | 1 + ...esizable-typedarray-bytelength-with-sab.js | 1 + .../resizable-typedarray-byteoffset-sab.js | 1 + .../resizable-typedarray-get-elem-with-sab.js | 1 + .../resizable-typedarray-has-elem-with-sab.js | 1 + ...e-typedarray-intrinsic-typedArrayLength.js | 1 + .../resizable-typedarray-length-with-sab.js | 1 + .../resizable-typedarray-set-elem-with-sab.js | 1 + .../warp/throw-exception-stack-location.js | 1 + js/src/moz.build | 12 +- js/src/night/README.md | 254 + js/src/night/build_nightmonkey.py | 44 + js/src/night/build_wasm_jit_runner.py | 48 + js/src/night/compiler/Cargo.toml | 20 + js/src/night/compiler/build.rs | 349 + js/src/night/compiler/night-compiler.h | 230 + js/src/night/compiler/src/bytecode.rs | 1082 +++ js/src/night/compiler/src/constants.rs | 426 ++ js/src/night/compiler/src/disasm.rs | 214 + js/src/night/compiler/src/env_regions.rs | 14 + js/src/night/compiler/src/facts.rs | 563 ++ js/src/night/compiler/src/ids.rs | 476 ++ js/src/night/compiler/src/lib.rs | 237 + .../night/compiler/src/likelier/builtins.rs | 621 ++ js/src/night/compiler/src/likelier/calls.rs | 1035 +++ js/src/night/compiler/src/likelier/dump.rs | 284 + js/src/night/compiler/src/likelier/effects.rs | 601 ++ js/src/night/compiler/src/likelier/emit.rs | 2919 ++++++++ js/src/night/compiler/src/likelier/engine.rs | 1083 +++ js/src/night/compiler/src/likelier/heap.rs | 2754 +++++++ js/src/night/compiler/src/likelier/mod.rs | 590 ++ js/src/night/compiler/src/likelier/scan.rs | 1654 +++++ js/src/night/compiler/src/likelier/stats.rs | 107 + js/src/night/compiler/src/likelier/types.rs | 1359 ++++ js/src/night/compiler/src/likelier/viz.rs | 179 + js/src/night/compiler/src/opsem.rs | 1095 +++ js/src/night/compiler/src/options.rs | 170 + js/src/night/compiler/src/region_shape.rs | 16 + js/src/night/compiler/src/source.rs | 291 + js/src/night/compiler/src/source/dump.rs | 192 + js/src/night/compiler/src/source/ffi.rs | 493 ++ js/src/night/compiler/src/view.rs | 48 + js/src/night/compiler/src/wasm/bbv/abi.rs | 403 + js/src/night/compiler/src/wasm/bbv/arith.rs | 1530 ++++ js/src/night/compiler/src/wasm/bbv/arms.rs | 204 + .../night/compiler/src/wasm/bbv/blockcen.rs | 474 ++ js/src/night/compiler/src/wasm/bbv/call.rs | 3081 ++++++++ js/src/night/compiler/src/wasm/bbv/cfg.rs | 627 ++ js/src/night/compiler/src/wasm/bbv/compare.rs | 766 ++ js/src/night/compiler/src/wasm/bbv/ctx.rs | 1497 ++++ js/src/night/compiler/src/wasm/bbv/element.rs | 1662 +++++ js/src/night/compiler/src/wasm/bbv/emit.rs | 2239 ++++++ js/src/night/compiler/src/wasm/bbv/facts.rs | 1702 +++++ js/src/night/compiler/src/wasm/bbv/frame.rs | 2352 ++++++ .../night/compiler/src/wasm/bbv/generator.rs | 462 ++ js/src/night/compiler/src/wasm/bbv/gname.rs | 880 +++ js/src/night/compiler/src/wasm/bbv/inline.rs | 1039 +++ js/src/night/compiler/src/wasm/bbv/licm.rs | 484 ++ js/src/night/compiler/src/wasm/bbv/live.rs | 414 ++ js/src/night/compiler/src/wasm/bbv/mod.rs | 2428 +++++++ js/src/night/compiler/src/wasm/bbv/object.rs | 646 ++ js/src/night/compiler/src/wasm/bbv/ops.rs | 1861 +++++ js/src/night/compiler/src/wasm/bbv/outline.rs | 31 + js/src/night/compiler/src/wasm/bbv/predict.rs | 401 + .../night/compiler/src/wasm/bbv/property.rs | 2349 ++++++ .../night/compiler/src/wasm/bbv/redundant.rs | 310 + js/src/night/compiler/src/wasm/bbv/version.rs | 3595 +++++++++ js/src/night/compiler/src/wasm/bbv/viz.rs | 987 +++ js/src/night/compiler/src/wasm/effects.rs | 523 ++ js/src/night/compiler/src/wasm/inprocess.rs | 959 +++ js/src/night/compiler/src/wasm/mod.rs | 2424 +++++++ js/src/night/compiler/src/wasm/regex.rs | 1645 +++++ js/src/night/compiler/src/wasm/translate.rs | 6464 +++++++++++++++++ js/src/night/configs/mozconfig-ion | 12 + js/src/night/configs/mozconfig-ion-jitdump | 13 + js/src/night/configs/mozconfig-native | 15 + js/src/night/configs/mozconfig-nightmonkey | 31 + .../configs/mozconfig-nightmonkey-inprocess | 33 + js/src/night/configs/mozconfig-wasm | 16 + js/src/night/configs/mozconfig-weval | 23 + js/src/night/docs/DESIGN.md | 4069 +++++++++++ js/src/night/docs/TODO | 8 + js/src/night/inproc-shell.sh | 58 + js/src/night/moz.build | 28 + js/src/night/nightmonkey/.gitignore | 1 + js/src/night/nightmonkey/Cargo.lock | 2739 +++++++ js/src/night/nightmonkey/Cargo.toml | 25 + js/src/night/nightmonkey/src/finder.rs | 81 + js/src/night/nightmonkey/src/image.rs | 166 + js/src/night/nightmonkey/src/main.rs | 741 ++ js/src/night/nightmonkey/src/strip.rs | 66 + js/src/night/nightmonkey/src/wizen.rs | 54 + js/src/night/runtime/Night.h | 96 + js/src/night/runtime/NightEntry.cpp | 405 ++ js/src/night/runtime/NightEntry.h | 69 + js/src/night/runtime/NightEnv.h | 140 + js/src/night/runtime/NightGenerator.cpp | 253 + js/src/night/runtime/NightGenerator.h | 72 + js/src/night/runtime/NightHelperList.h | 256 + js/src/night/runtime/NightInlineCaches.cpp | 505 ++ js/src/night/runtime/NightInlineCaches.h | 98 + js/src/night/runtime/NightInlineHeap.cpp | 348 + js/src/night/runtime/NightInlineHeap.h | 112 + js/src/night/runtime/NightInproc.cpp | 280 + js/src/night/runtime/NightInprocHost.cpp | 48 + js/src/night/runtime/NightInprocHost.h | 35 + js/src/night/runtime/NightOps.cpp | 773 ++ js/src/night/runtime/NightOps.h | 129 + js/src/night/runtime/NightRegExp.cpp | 293 + js/src/night/runtime/NightRegionShape.h | 162 + js/src/night/runtime/NightRegistration.cpp | 330 + js/src/night/runtime/NightRegistration.h | 263 + js/src/night/runtime/NightRuntime.cpp | 5772 +++++++++++++++ js/src/night/runtime/NightRuntime.h | 878 +++ js/src/night/runtime/NightRuntimeData.h | 44 + js/src/night/runtime/NightRuntimeSlots.cpp | 28 + js/src/night/runtime/NightRuntimeSlots.h | 29 + js/src/night/runtime/NightSnapshotExtras.cpp | 722 ++ js/src/night/runtime/NightSnapshotExtras.h | 50 + js/src/night/runtime/NightStack.cpp | 41 + js/src/night/runtime/NightStack.h | 91 + js/src/night/runtime/moz.build | 52 + js/src/night/snapshot/Cargo.toml | 9 + js/src/night/snapshot/build.rs | 65 + js/src/night/snapshot/night-snapshot.h | 51 + js/src/night/snapshot/src/ffi.rs | 136 + js/src/night/snapshot/src/layout.rs | 39 + js/src/night/snapshot/src/lib.rs | 25 + js/src/night/snapshot/src/mem.rs | 48 + js/src/night/snapshot/src/registration.rs | 352 + js/src/night/snapshot/src/walker.rs | 668 ++ js/src/night/tools/annotate.py | 114 + js/src/night/tools/blockprof.py | 322 + js/src/night/tools/blockprof.sh | 64 + js/src/night/tools/bucketize.py | 71 + js/src/night/tools/census.py | 99 + js/src/night/tools/codesize/README.md | 37 + js/src/night/tools/codesize/ab.sh | 28 + js/src/night/tools/codesize/cwasm_syms.py | 34 + js/src/night/tools/codesize/disan.py | 108 + js/src/night/tools/codesize/footprint.py | 93 + js/src/night/tools/codesize/hotann.py | 78 + js/src/night/tools/codesize/hotmix.py | 127 + js/src/night/tools/codesize/opsize.py | 78 + js/src/night/tools/codesize/pcmp.py | 64 + js/src/night/tools/codesize/pstat.py | 155 + js/src/night/tools/codesize/ptable.py | 88 + js/src/night/tools/codesize/roles.py | 147 + js/src/night/tools/codesize/sizecmp.py | 48 + js/src/night/tools/codesize/wasmnames.py | 67 + js/src/night/tools/ctxdiff.py | 238 + js/src/night/tools/diff-facts.py | 103 + js/src/night/tools/dirtystart.py | 68 + js/src/night/tools/domfact.py | 278 + js/src/night/tools/downstream.py | 130 + js/src/night/tools/falloff.py | 70 + js/src/night/tools/guards.py | 233 + js/src/night/tools/hotloop.py | 258 + js/src/night/tools/machperf.sh | 102 + js/src/night/tools/nativeprof.sh | 65 + js/src/night/tools/natop.sh | 96 + js/src/night/tools/opclass.py | 214 + js/src/night/tools/opprof.py | 148 + js/src/night/tools/pairab.sh | 58 + js/src/night/tools/qp4.sh | 10 + js/src/night/tools/quickperf.sh | 42 + js/src/night/tools/rejoin.py | 89 + js/src/night/tools/relyrank.py | 128 + js/src/night/tools/rootcause.py | 164 + js/src/night/tools/segloss.py | 165 + js/src/night/tools/site.py | 73 + js/src/night/tools/sizecmp.sh | 28 + js/src/night/tools/var-table.sh | 56 + js/src/night/tools/viz.py | 1570 ++++ js/src/night/wasm-jit-runner/.gitignore | 2 + js/src/night/wasm-jit-runner/Cargo.lock | 2493 +++++++ js/src/night/wasm-jit-runner/Cargo.toml | 17 + js/src/night/wasm-jit-runner/README.md | 127 + .../wasm-jit-runner/guest/example/build.sh | 17 + .../guest/example/test_guest.c | 157 + js/src/night/wasm-jit-runner/guest/wasm_add.h | 81 + .../night/wasm-jit-runner/guest/wasm_build.h | 236 + js/src/night/wasm-jit-runner/src/addfuncs.rs | 532 ++ js/src/night/wasm-jit-runner/src/cache.rs | 64 + js/src/night/wasm-jit-runner/src/main.rs | 195 + js/src/night/wasm-jit-runner/src/modedit.rs | 210 + js/src/night/wasm-jit-runner/test.sh | 10 + js/src/rust/Cargo.toml | 1 + js/src/rust/moz.build | 3 + js/src/rust/shared/Cargo.toml | 3 + js/src/rust/shared/lib.rs | 6 + js/src/shell/CommonShellGlobals.cpp | 107 + js/src/shell/CommonShellGlobals.h | 46 + js/src/shell/js.cpp | 192 +- js/src/shell/moz.build | 19 +- js/src/shell/wizer.cpp | 28 +- js/src/tests/jstests.list | 60 + js/src/vm/BytecodeUtil.cpp | 11 +- js/src/vm/CommonPropertyNames.h | 3 + js/src/vm/EnvironmentObject.cpp | 12 + js/src/vm/Interpreter.cpp | 61 + js/src/vm/JSContext.cpp | 11 + js/src/vm/JSContext.h | 9 +- js/src/vm/JSFunction-inl.h | 10 + js/src/vm/JSObject.cpp | 6 + js/src/vm/JSObject.h | 154 +- js/src/vm/JSScript.cpp | 12 + js/src/vm/JSScript.h | 30 + js/src/vm/MatchPairs.h | 9 + js/src/vm/NativeObject.cpp | 23 + js/src/vm/NativeObject.h | 25 +- js/src/vm/RealmFuses.cpp | 25 + js/src/vm/RealmFuses.h | 25 +- js/src/vm/RegExpObject.cpp | 14 + js/src/vm/RegExpShared.h | 11 + js/src/vm/RegExpStatics.h | 30 + js/src/vm/Runtime.h | 18 + js/src/vm/Scope.h | 10 + js/src/vm/Shape.cpp | 44 +- js/src/vm/Shape.h | 2 +- js/src/vm/SharedStencil.h | 11 + js/src/vm/StaticStrings.h | 7 + js/src/vm/Watchtower.cpp | 30 + 454 files changed, 97830 insertions(+), 161 deletions(-) create mode 100644 js/public/NightMonkey.h create mode 100644 js/src/jit-test/tests/night/define-elem-over-sparse.js create mode 100644 js/src/jit-test/tests/night/exception-unwinds-lexical-env.js create mode 100644 js/src/jit-test/tests/night/function-name-lazy-resolve.js create mode 100644 js/src/jit-test/tests/night/gc-callback-keeps-cache-purge.js create mode 100644 js/src/jit-test/tests/night/regexp-exec-lastindex-read.js create mode 100644 js/src/night/README.md create mode 100644 js/src/night/build_nightmonkey.py create mode 100644 js/src/night/build_wasm_jit_runner.py create mode 100644 js/src/night/compiler/Cargo.toml create mode 100644 js/src/night/compiler/build.rs create mode 100644 js/src/night/compiler/night-compiler.h create mode 100644 js/src/night/compiler/src/bytecode.rs create mode 100644 js/src/night/compiler/src/constants.rs create mode 100644 js/src/night/compiler/src/disasm.rs create mode 100644 js/src/night/compiler/src/env_regions.rs create mode 100644 js/src/night/compiler/src/facts.rs create mode 100644 js/src/night/compiler/src/ids.rs create mode 100644 js/src/night/compiler/src/lib.rs create mode 100644 js/src/night/compiler/src/likelier/builtins.rs create mode 100644 js/src/night/compiler/src/likelier/calls.rs create mode 100644 js/src/night/compiler/src/likelier/dump.rs create mode 100644 js/src/night/compiler/src/likelier/effects.rs create mode 100644 js/src/night/compiler/src/likelier/emit.rs create mode 100644 js/src/night/compiler/src/likelier/engine.rs create mode 100644 js/src/night/compiler/src/likelier/heap.rs create mode 100644 js/src/night/compiler/src/likelier/mod.rs create mode 100644 js/src/night/compiler/src/likelier/scan.rs create mode 100644 js/src/night/compiler/src/likelier/stats.rs create mode 100644 js/src/night/compiler/src/likelier/types.rs create mode 100644 js/src/night/compiler/src/likelier/viz.rs create mode 100644 js/src/night/compiler/src/opsem.rs create mode 100644 js/src/night/compiler/src/options.rs create mode 100644 js/src/night/compiler/src/region_shape.rs create mode 100644 js/src/night/compiler/src/source.rs create mode 100644 js/src/night/compiler/src/source/dump.rs create mode 100644 js/src/night/compiler/src/source/ffi.rs create mode 100644 js/src/night/compiler/src/view.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/abi.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/arith.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/arms.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/blockcen.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/call.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/cfg.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/compare.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/ctx.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/element.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/emit.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/facts.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/frame.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/generator.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/gname.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/inline.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/licm.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/live.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/mod.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/object.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/ops.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/outline.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/predict.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/property.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/redundant.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/version.rs create mode 100644 js/src/night/compiler/src/wasm/bbv/viz.rs create mode 100644 js/src/night/compiler/src/wasm/effects.rs create mode 100644 js/src/night/compiler/src/wasm/inprocess.rs create mode 100644 js/src/night/compiler/src/wasm/mod.rs create mode 100644 js/src/night/compiler/src/wasm/regex.rs create mode 100644 js/src/night/compiler/src/wasm/translate.rs create mode 100644 js/src/night/configs/mozconfig-ion create mode 100644 js/src/night/configs/mozconfig-ion-jitdump create mode 100644 js/src/night/configs/mozconfig-native create mode 100644 js/src/night/configs/mozconfig-nightmonkey create mode 100644 js/src/night/configs/mozconfig-nightmonkey-inprocess create mode 100644 js/src/night/configs/mozconfig-wasm create mode 100644 js/src/night/configs/mozconfig-weval create mode 100644 js/src/night/docs/DESIGN.md create mode 100644 js/src/night/docs/TODO create mode 100755 js/src/night/inproc-shell.sh create mode 100644 js/src/night/moz.build create mode 100644 js/src/night/nightmonkey/.gitignore create mode 100644 js/src/night/nightmonkey/Cargo.lock create mode 100644 js/src/night/nightmonkey/Cargo.toml create mode 100644 js/src/night/nightmonkey/src/finder.rs create mode 100644 js/src/night/nightmonkey/src/image.rs create mode 100644 js/src/night/nightmonkey/src/main.rs create mode 100644 js/src/night/nightmonkey/src/strip.rs create mode 100644 js/src/night/nightmonkey/src/wizen.rs create mode 100644 js/src/night/runtime/Night.h create mode 100644 js/src/night/runtime/NightEntry.cpp create mode 100644 js/src/night/runtime/NightEntry.h create mode 100644 js/src/night/runtime/NightEnv.h create mode 100644 js/src/night/runtime/NightGenerator.cpp create mode 100644 js/src/night/runtime/NightGenerator.h create mode 100644 js/src/night/runtime/NightHelperList.h create mode 100644 js/src/night/runtime/NightInlineCaches.cpp create mode 100644 js/src/night/runtime/NightInlineCaches.h create mode 100644 js/src/night/runtime/NightInlineHeap.cpp create mode 100644 js/src/night/runtime/NightInlineHeap.h create mode 100644 js/src/night/runtime/NightInproc.cpp create mode 100644 js/src/night/runtime/NightInprocHost.cpp create mode 100644 js/src/night/runtime/NightInprocHost.h create mode 100644 js/src/night/runtime/NightOps.cpp create mode 100644 js/src/night/runtime/NightOps.h create mode 100644 js/src/night/runtime/NightRegExp.cpp create mode 100644 js/src/night/runtime/NightRegionShape.h create mode 100644 js/src/night/runtime/NightRegistration.cpp create mode 100644 js/src/night/runtime/NightRegistration.h create mode 100644 js/src/night/runtime/NightRuntime.cpp create mode 100644 js/src/night/runtime/NightRuntime.h create mode 100644 js/src/night/runtime/NightRuntimeData.h create mode 100644 js/src/night/runtime/NightRuntimeSlots.cpp create mode 100644 js/src/night/runtime/NightRuntimeSlots.h create mode 100644 js/src/night/runtime/NightSnapshotExtras.cpp create mode 100644 js/src/night/runtime/NightSnapshotExtras.h create mode 100644 js/src/night/runtime/NightStack.cpp create mode 100644 js/src/night/runtime/NightStack.h create mode 100644 js/src/night/runtime/moz.build create mode 100644 js/src/night/snapshot/Cargo.toml create mode 100644 js/src/night/snapshot/build.rs create mode 100644 js/src/night/snapshot/night-snapshot.h create mode 100644 js/src/night/snapshot/src/ffi.rs create mode 100644 js/src/night/snapshot/src/layout.rs create mode 100644 js/src/night/snapshot/src/lib.rs create mode 100644 js/src/night/snapshot/src/mem.rs create mode 100644 js/src/night/snapshot/src/registration.rs create mode 100644 js/src/night/snapshot/src/walker.rs create mode 100644 js/src/night/tools/annotate.py create mode 100644 js/src/night/tools/blockprof.py create mode 100755 js/src/night/tools/blockprof.sh create mode 100644 js/src/night/tools/bucketize.py create mode 100755 js/src/night/tools/census.py create mode 100644 js/src/night/tools/codesize/README.md create mode 100755 js/src/night/tools/codesize/ab.sh create mode 100644 js/src/night/tools/codesize/cwasm_syms.py create mode 100644 js/src/night/tools/codesize/disan.py create mode 100644 js/src/night/tools/codesize/footprint.py create mode 100644 js/src/night/tools/codesize/hotann.py create mode 100644 js/src/night/tools/codesize/hotmix.py create mode 100644 js/src/night/tools/codesize/opsize.py create mode 100644 js/src/night/tools/codesize/pcmp.py create mode 100644 js/src/night/tools/codesize/pstat.py create mode 100644 js/src/night/tools/codesize/ptable.py create mode 100644 js/src/night/tools/codesize/roles.py create mode 100644 js/src/night/tools/codesize/sizecmp.py create mode 100644 js/src/night/tools/codesize/wasmnames.py create mode 100755 js/src/night/tools/ctxdiff.py create mode 100644 js/src/night/tools/diff-facts.py create mode 100644 js/src/night/tools/dirtystart.py create mode 100755 js/src/night/tools/domfact.py create mode 100644 js/src/night/tools/downstream.py create mode 100644 js/src/night/tools/falloff.py create mode 100755 js/src/night/tools/guards.py create mode 100755 js/src/night/tools/hotloop.py create mode 100755 js/src/night/tools/machperf.sh create mode 100755 js/src/night/tools/nativeprof.sh create mode 100755 js/src/night/tools/natop.sh create mode 100755 js/src/night/tools/opclass.py create mode 100644 js/src/night/tools/opprof.py create mode 100755 js/src/night/tools/pairab.sh create mode 100755 js/src/night/tools/qp4.sh create mode 100755 js/src/night/tools/quickperf.sh create mode 100644 js/src/night/tools/rejoin.py create mode 100644 js/src/night/tools/relyrank.py create mode 100644 js/src/night/tools/rootcause.py create mode 100755 js/src/night/tools/segloss.py create mode 100644 js/src/night/tools/site.py create mode 100755 js/src/night/tools/sizecmp.sh create mode 100755 js/src/night/tools/var-table.sh create mode 100644 js/src/night/tools/viz.py create mode 100644 js/src/night/wasm-jit-runner/.gitignore create mode 100644 js/src/night/wasm-jit-runner/Cargo.lock create mode 100644 js/src/night/wasm-jit-runner/Cargo.toml create mode 100644 js/src/night/wasm-jit-runner/README.md create mode 100755 js/src/night/wasm-jit-runner/guest/example/build.sh create mode 100644 js/src/night/wasm-jit-runner/guest/example/test_guest.c create mode 100644 js/src/night/wasm-jit-runner/guest/wasm_add.h create mode 100644 js/src/night/wasm-jit-runner/guest/wasm_build.h create mode 100644 js/src/night/wasm-jit-runner/src/addfuncs.rs create mode 100644 js/src/night/wasm-jit-runner/src/cache.rs create mode 100644 js/src/night/wasm-jit-runner/src/main.rs create mode 100644 js/src/night/wasm-jit-runner/src/modedit.rs create mode 100755 js/src/night/wasm-jit-runner/test.sh create mode 100644 js/src/shell/CommonShellGlobals.cpp create mode 100644 js/src/shell/CommonShellGlobals.h diff --git a/.cargo/config.toml.in b/.cargo/config.toml.in index c7b07a63ea099..0044a0b8edd2d 100644 --- a/.cargo/config.toml.in +++ b/.cargo/config.toml.in @@ -2,8 +2,29 @@ # It was generated by `mach vendor rust`. # Please do not edit. -[source.crates-io] -replace-with = "vendored-sources" +# DEVELOPMENT OVERRIDE for this fork: crates.io is not replaced by +# third_party/rust, so the AOT compiler's dependencies (waffle and the +# wasm-tools crates) are fetched from the network and third_party/ carries no +# NightMonkey diff. +# +# This is a deliberate choice for this tree, not a temporary state. The fork +# has no offline-build constraint; mozilla-central does, which is why the +# stanza exists upstream. Integrating with the vendoring scheme is a question +# for an upstreaming effort, if there is one -- not a prerequisite for working +# here. +# +# It has to be tree-wide rather than NightMonkey-scoped: `night-compiler` is a +# workspace member (the in-process lane links it into the jsrust staticlib), +# so `cargo metadata` resolves waffle for every build in the tree, including +# builds that never compile it. +# +# Locally patched crates are unaffected -- those go through [patch.crates-io] +# onto build/rust/* paths -- and the git-sourced crates below keep their +# vendored replacement. To restore upstream behavior, vendor the compiler's +# dependencies and re-enable: +# +# [source.crates-io] +# replace-with = "vendored-sources" [source."git+https://github.com/FirefoxGraphics/aa-stroke?rev=5776bdfc8ad664a1503db668fab397d818a5f98a"] git = "https://github.com/FirefoxGraphics/aa-stroke" diff --git a/.prettierignore b/.prettierignore index e5da3b0de2e9c..b6251e6dd23fa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1140,6 +1140,7 @@ js/examples/ js/public/ js/src/devtools/ js/src/jit-test/ +js/src/night/tests/ js/src/tests/ js/src/Y.js diff --git a/Cargo.lock b/Cargo.lock index 723445447f491..430fc5271d761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2372,6 +2372,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "gecko-fuzz-targets" version = "0.1.0" @@ -2954,6 +2963,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" version = "0.10.999" @@ -3422,12 +3437,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3623,6 +3638,8 @@ dependencies = [ "gluesmith", "icu_capi", "mozglue-static", + "night-compiler", + "night-snapshot", "unicode-bidi-ffi", ] @@ -3772,6 +3789,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.176" @@ -4973,6 +4996,27 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "night-compiler" +version = "0.1.0" +dependencies = [ + "log", + "mozilla-central-workspace-hack", + "rustc-hash 2.1.1", + "waffle", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", +] + +[[package]] +name = "night-snapshot" +version = "0.1.0" +dependencies = [ + "anyhow", + "night-compiler", + "rustc-hash 2.1.1", +] + [[package]] name = "nix" version = "0.29.0" @@ -7693,6 +7737,20 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "waffle" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9d7881318c4957534037f7595778f89bc8aded2d853664358050bca3fb3882" +dependencies = [ + "anyhow", + "fxhash", + "log", + "smallvec", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", +] + [[package]] name = "walkdir" version = "2.3.2" @@ -7768,7 +7826,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29cbbd772edcb8e7d524a82ee8cef8dd046fc14033796a754c3ad246d019fa54" dependencies = [ "leb128", - "wasmparser", + "wasmparser 0.219.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6" +dependencies = [ + "leb128fmt", + "wasmparser 0.248.0", ] [[package]] @@ -7782,7 +7850,7 @@ dependencies = [ "flagset", "indexmap", "leb128", - "wasm-encoder", + "wasm-encoder 0.219.1", ] [[package]] @@ -7795,6 +7863,17 @@ dependencies = [ "indexmap", ] +[[package]] +name = "wasmparser" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" +dependencies = [ + "bitflags 2.9.0", + "indexmap", + "semver", +] + [[package]] name = "wast" version = "219.0.1" @@ -7805,7 +7884,7 @@ dependencies = [ "leb128", "memchr", "unicode-width 0.1.999", - "wasm-encoder", + "wasm-encoder 0.219.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 35e725043bce0..38d6a2288bc7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "browser/app/nmhproxy/", "js/src/rust", + "js/src/night/compiler", "netwerk/base/idna_glue", "netwerk/test/http3server", "security/manager/ssl/abridged_certs", diff --git a/build/workspace-hack/Cargo.toml b/build/workspace-hack/Cargo.toml index bc8ffd62af49b..1f8c1e90a5d79 100644 --- a/build/workspace-hack/Cargo.toml +++ b/build/workspace-hack/Cargo.toml @@ -273,6 +273,7 @@ gkrust = ["dep:allocator-api2", "dep:arrayvec", "dep:bindgen", "dep:bitflags", " gkrust-gtest = ["gkrust"] http3server = ["dep:allocator-api2", "dep:arrayvec", "dep:bindgen", "dep:bitflags", "dep:byteorder", "dep:bytes", "dep:chrono", "dep:clap", "dep:dist-bin", "dep:env_logger", "dep:fnv", "dep:form_urlencoded", "dep:futures", "dep:futures-channel", "dep:futures-core", "dep:futures-executor", "dep:futures-sink", "dep:futures-util", "dep:getrandom", "dep:hashbrown", "dep:hex", "dep:hyper", "dep:icu_locale_core", "dep:icu_properties", "dep:idna", "dep:indexmap", "dep:itertools", "dep:log", "dep:memchr", "dep:mio", "dep:nom", "dep:num-integer", "dep:num-traits", "dep:once_cell", "dep:percent-encoding", "dep:regex", "dep:semver", "dep:serde_json", "dep:smallvec", "dep:stable_deref_trait", "dep:strsim", "dep:time", "dep:time-macros", "dep:tinystr", "dep:tokio", "dep:tokio-util", "dep:toml", "dep:tracing", "dep:unicode-bidi", "dep:url", "dep:windows", "dep:windows-sys", "dep:yoke", "dep:zerocopy", "dep:zerofrom", "dep:zerovec"] jsrust = ["dep:allocator-api2", "dep:arrayvec", "dep:byteorder", "dep:cc", "dep:env_logger", "dep:form_urlencoded", "dep:getrandom", "dep:hashbrown", "dep:icu_locale_core", "dep:icu_properties", "dep:idna", "dep:indexmap", "dep:log", "dep:memchr", "dep:num-traits", "dep:once_cell", "dep:percent-encoding", "dep:semver", "dep:smallvec", "dep:stable_deref_trait", "dep:tinystr", "dep:unicode-bidi", "dep:url", "dep:yoke", "dep:zerocopy", "dep:zerofrom", "dep:zerovec"] +night-compiler = [] minidump-analyzer-export = ["dep:allocator-api2", "dep:arrayvec", "dep:bitflags", "dep:byteorder", "dep:clap", "dep:env_logger", "dep:futures-channel", "dep:futures-core", "dep:futures-executor", "dep:futures-sink", "dep:futures-util", "dep:getrandom", "dep:hashbrown", "dep:hex", "dep:indexmap", "dep:log", "dep:memchr", "dep:nom", "dep:num-traits", "dep:object", "dep:once_cell", "dep:scroll", "dep:serde_json", "dep:time", "dep:time-macros", "dep:tracing", "dep:uuid", "dep:windows-sys", "dep:zerocopy"] mozwer_s = ["dep:allocator-api2", "dep:byteorder", "dep:getrandom", "dep:hashbrown", "dep:indexmap", "dep:log", "dep:once_cell", "dep:scroll", "dep:serde_json", "dep:uuid", "dep:windows-sys", "dep:zerocopy"] nmhproxy = ["dep:allocator-api2", "dep:bitflags", "dep:byteorder", "dep:form_urlencoded", "dep:hashbrown", "dep:icu_locale_core", "dep:icu_properties", "dep:idna", "dep:indexmap", "dep:once_cell", "dep:percent-encoding", "dep:serde_json", "dep:smallvec", "dep:stable_deref_trait", "dep:tinystr", "dep:unicode-bidi", "dep:url", "dep:windows-sys", "dep:yoke", "dep:zerocopy", "dep:zerofrom", "dep:zerovec"] diff --git a/config/check_spidermonkey_style.py b/config/check_spidermonkey_style.py index 791b1c9234df4..63635c37b045f 100644 --- a/config/check_spidermonkey_style.py +++ b/config/check_spidermonkey_style.py @@ -48,6 +48,10 @@ "js/src/devtools/", # auxiliary stuff "js/src/editline/", # imported code "js/src/gdb/", # auxiliary stuff + "js/src/night/nightmonkey/", # snapshot transform tool (own crate) + "js/src/night/snapshot/", # own crate (build outputs) + "js/src/night/snapshot-dump/", # host dump tool (own crate) + "js/src/night/wasm-jit-runner/", # vendored tool (own crate) "js/src/vtune/", # imported code "js/src/zydis/", # imported code "js/src/xsum/", # imported code @@ -104,6 +108,7 @@ "unicode/uniset.h", # ICU "unicode/unistr.h", # ICU "unicode/utypes.h", # ICU + "night/snapshot/night-snapshot.h", # lives in an ignored crate dir "vtune/VTuneWrapper.h", # VTune "wasm/WasmBuiltinModuleGenerated.h", # generated in $OBJDIR" "zydis/ZydisAPI.h", # Zydis diff --git a/eslint-ignores.config.mjs b/eslint-ignores.config.mjs index 0cfd7e02ad58c..3cfe5fb574aec 100644 --- a/eslint-ignores.config.mjs +++ b/eslint-ignores.config.mjs @@ -189,6 +189,7 @@ export default [ "js/public/", "js/src/devtools/", "js/src/jit-test/", + "js/src/night/tests/", "js/src/tests/", "js/src/Y.js", diff --git a/js/moz.configure b/js/moz.configure index e6944cda3fb9a..4e754020b8008 100644 --- a/js/moz.configure +++ b/js/moz.configure @@ -263,6 +263,75 @@ set_config( depends_if("--enable-aot-ics-enforce")(lambda _: True), ) +# NightMonkey, the AOT compilation tier (wasm32 shell targets only): links +# the night runtime (js/src/night/runtime/) into the shell, supports wizer +# snapshot registration, and builds the `nightmonkey` host binary (the +# snapshot AOT compiler). +option( + "--enable-nightmonkey", + default=False, + help="{Enable|Disable} the NightMonkey AOT tier (wasm32 targets only)", +) + + +@depends("--enable-nightmonkey", target) +def nightmonkey(value, target): + if value: + if target.cpu != "wasm32": + die( + "--enable-nightmonkey requires a wasm32 target (e.g. --target=wasm32-unknown-wasi)" + ) + return True + + +set_config("ENABLE_JS_NIGHTMONKEY", nightmonkey) +set_define("ENABLE_JS_NIGHTMONKEY", nightmonkey) + +# NightMonkey in-process compilation (test-only): compiles scripts inside +# the running wasm shell (--night-inprocess) and injects the compiled +# bodies through the wasm-jit-runner hostcalls, whose imports it adds to +# the shell module; also builds the wasm-jit-runner host binary. +option( + "--enable-nightmonkey-inprocess", + default=False, + help="{Enable|Disable} NightMonkey in-process compilation " + "(requires --enable-nightmonkey)", +) + + +@depends("--enable-nightmonkey-inprocess", nightmonkey) +def nightmonkey_inprocess(value, nightmonkey): + if value: + if not nightmonkey: + die("--enable-nightmonkey-inprocess requires --enable-nightmonkey") + return True + + +set_config("ENABLE_JS_NIGHTMONKEY_INPROCESS", nightmonkey_inprocess) +set_define("ENABLE_JS_NIGHTMONKEY_INPROCESS", nightmonkey_inprocess) + +# NightMonkey runtime diagnostics: opt-in summaries and crash-on-failure in +# the in-process test lane. Off even in debug builds, because crashing on a +# failed in-process batch is right for a test lane and wrong for a developer +# who merely ran the shell without the runner. +option( + "--enable-nightmonkey-debug", + default=False, + help="{Enable|Disable} NightMonkey runtime diagnostics " + "(requires --enable-nightmonkey)", +) + + +@depends("--enable-nightmonkey-debug", nightmonkey) +def nightmonkey_debug(value, nightmonkey): + if value: + if not nightmonkey: + die("--enable-nightmonkey-debug requires --enable-nightmonkey") + return True + + +set_config("NIGHTMONKEY_DEBUG", nightmonkey_debug) +set_define("NIGHTMONKEY_DEBUG", nightmonkey_debug) # Enable JS Streams # =================================================== diff --git a/js/public/NightMonkey.h b/js/public/NightMonkey.h new file mode 100644 index 0000000000000..81c45dca94273 --- /dev/null +++ b/js/public/NightMonkey.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef js_NightMonkey_h +#define js_NightMonkey_h + +#include "jstypes.h" + +#include "js/RootingAPI.h" + +struct JS_PUBLIC_API JSContext; + +namespace JS { + +extern JS_PUBLIC_API bool NightRegisterRoot(JSContext* cx, + Handle script, + bool executedAtInit); + +extern JS_PUBLIC_API bool NightCaptureSnapshotExtras(JSContext* cx, + Handle root); + +extern JS_PUBLIC_API bool NightCaptureSnapshotHeap(JSContext* cx); + +extern JS_PUBLIC_API bool NightActivate(JSContext* cx); + +} // namespace JS + +#endif // js_NightMonkey_h diff --git a/js/src/builtin/RegExp.cpp b/js/src/builtin/RegExp.cpp index 943ea0e6ed005..4e8ad0e3791e1 100644 --- a/js/src/builtin/RegExp.cpp +++ b/js/src/builtin/RegExp.cpp @@ -302,8 +302,8 @@ bool js::CreateRegExpMatchResult(JSContext* cx, HandleRegExpShared re, return true; } -static int32_t CreateRegExpSearchResult(JSContext* cx, - const MatchPairs& matches) { +namespace js { +int32_t CreateRegExpSearchResult(JSContext* cx, const MatchPairs& matches) { MOZ_ASSERT(matches[0].start >= 0); MOZ_ASSERT(matches[0].limit >= 0); @@ -317,6 +317,7 @@ static int32_t CreateRegExpSearchResult(JSContext* cx, cx->regExpSearcherLastLimit = matches[0].limit; return matches[0].start; } +} // namespace js /* * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 @@ -332,9 +333,15 @@ static RegExpRunStatus ExecuteRegExpImpl(JSContext* cx, RegExpStatics* res, /* Out of spec: Update RegExpStatics. */ if (status == RegExpRunStatus::Success && res) { +#ifdef ENABLE_JS_NIGHTMONKEY + // Lazy scheme: record the replay recipe instead of copying the pairs + // per match; the first statics read re-executes (executeLazy). + res->updateLazily(cx, input, re, searchIndex); +#else if (!res->updateFromMatchPairs(cx, input, *matches)) { return RegExpRunStatus::Error; } +#endif } return status; } @@ -531,9 +538,10 @@ bool js::IsRegExp(JSContext* cx, HandleValue value, bool* result) { // The "lastIndex" property is non-configurable, but it can be made // non-writable. If CalledFromJit is true, we have emitted guards to ensure it's // writable. +namespace js { template -static bool SetLastIndex(JSContext* cx, Handle regexp, - int32_t lastIndex) { +bool SetLastIndex(JSContext* cx, Handle regexp, + int32_t lastIndex) { MOZ_ASSERT(lastIndex >= 0); if (CalledFromJit || MOZ_LIKELY(RegExpObject::isInitialShape(regexp)) || @@ -545,6 +553,11 @@ static bool SetLastIndex(JSContext* cx, Handle regexp, Rooted val(cx, Int32Value(lastIndex)); return SetProperty(cx, regexp, cx->names().lastIndex, val); } +#ifdef ENABLE_JS_NIGHTMONKEY +template bool SetLastIndex(JSContext* cx, Handle regexp, + int32_t lastIndex); +#endif +} // namespace js /* ES6 B.2.5.1. */ MOZ_ALWAYS_INLINE bool regexp_compile_impl(JSContext* cx, diff --git a/js/src/builtin/RegExp.h b/js/src/builtin/RegExp.h index 24ad7057809c5..8aca74c21a3f0 100644 --- a/js/src/builtin/RegExp.h +++ b/js/src/builtin/RegExp.h @@ -56,6 +56,37 @@ JSObject* InitRegExpClass(JSContext* cx, HandleObject obj); const MatchPairs& matches, MutableHandleValue rval); +#ifdef ENABLE_JS_NIGHTMONKEY +// RegExp.cpp internals reused by the AOT regexp fast paths below, which live +// in js/src/night/runtime/NightRegExp.cpp. `SetLastIndex` is declared without +// its default template argument (RegExp.cpp's definition supplies it), so +// callers here must name the specialization. +extern int32_t CreateRegExpSearchResult(JSContext* cx, + const MatchPairs& matches); + +template +bool SetLastIndex(JSContext* cx, Handle regexp, + int32_t lastIndex); + +// Collapsed AOT-matcher fast path for the RegExpMatcher (searcher=false) / +// RegExpSearcher (searcher=true) intrinsics; frame is the rooted AOT call +// frame [callee, this, regexp, string, lastIndex]. On *handled, the result +// has been written to frame[0]. *handled=false falls back to the native. +[[nodiscard]] extern bool NightRegExpBuiltinFast(JSContext* cx, + JS::Value* frame, + unsigned argc, bool searcher, + bool* handled); + +// Collapsed AOT fast path for the pristine RegExp.prototype.exec +// (forTest=false) / .test (forTest=true) callee-identity arm; frame is the +// rooted AOT call frame [callee, this(=regexp), string]. On *handled, the +// result (match array / null / boolean) is in frame[0]. test() allocates +// nothing on this path. +[[nodiscard]] extern bool NightRegExpExecTestFast(JSContext* cx, + JS::Value* frame, + bool forTest, bool* handled); +#endif + [[nodiscard]] extern bool RegExpMatcher(JSContext* cx, unsigned argc, Value* vp); diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp index 3d7a05c5c2bab..ec80dc16abf34 100644 --- a/js/src/builtin/String.cpp +++ b/js/src/builtin/String.cpp @@ -1834,7 +1834,7 @@ static MOZ_ALWAYS_INLINE bool ToRelativeStringIndex( * * ES2024 draft rev 7d2644968bd56d54d2886c012d18698ff3f72c35 */ -static bool str_charAt(JSContext* cx, unsigned argc, Value* vp) { +bool js::str_charAt(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "charAt"); CallArgs args = CallArgsFromVp(argc, vp); @@ -4345,6 +4345,15 @@ static bool StringClassFinish(JSContext* cx, HandleObject ctor, return false; } +#ifdef ENABLE_JS_NIGHTMONKEY + // Watchtower-watch the prototype and constructor so mutations of the + // char-op methods pop OptimizeStringCharOpsFuse. + if (!JSObject::setHasRealmFuseProperty(cx, proto) || + !JSObject::setHasRealmFuseProperty(cx, ctor)) { + return false; + } +#endif + return true; } diff --git a/js/src/builtin/String.h b/js/src/builtin/String.h index c4caed0376002..2c2a2ae24fd9a 100644 --- a/js/src/builtin/String.h +++ b/js/src/builtin/String.h @@ -34,6 +34,8 @@ extern bool str_startsWith(JSContext* cx, unsigned argc, Value* vp); extern bool str_toString(JSContext* cx, unsigned argc, Value* vp); +extern bool str_charAt(JSContext* cx, unsigned argc, Value* vp); + extern bool str_charCodeAt(JSContext* cx, unsigned argc, Value* vp); extern bool str_codePointAt(JSContext* cx, unsigned argc, Value* vp); diff --git a/js/src/gc/Nursery.h b/js/src/gc/Nursery.h index d282ddb6e09f2..cf1eefef9059c 100644 --- a/js/src/gc/Nursery.h +++ b/js/src/gc/Nursery.h @@ -134,7 +134,7 @@ class Nursery { // now succeed. [[nodiscard]] JS::GCReason handleAllocationFailure(); - static size_t nurseryCellHeaderSize() { + static constexpr size_t nurseryCellHeaderSize() { return sizeof(gc::NurseryCellHeader); } diff --git a/js/src/irregexp/RegExpAPI.cpp b/js/src/irregexp/RegExpAPI.cpp index 679f3856892bf..07b91fa0fa31b 100644 --- a/js/src/irregexp/RegExpAPI.cpp +++ b/js/src/irregexp/RegExpAPI.cpp @@ -33,11 +33,15 @@ #include "js/friend/ErrorMessages.h" // JSMSG_* #include "js/friend/StackLimits.h" // js::ReportOverRecursed #include "util/StringBuilder.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/NightRuntimeData.h" // js::night::NightRuntimeData (regex table cache) +#endif #include "vm/MatchPairs.h" #include "vm/PlainObject.h" #include "vm/RegExpShared.h" namespace js { + namespace irregexp { using mozilla::AssertedCast; @@ -916,9 +920,21 @@ RegExpRunStatus Interpret(JSContext* cx, MutableHandleRegExpShared re, return status; } +#ifdef ENABLE_JS_NIGHTMONKEY +// TryNightRegexMatch (js/src/night/runtime/NightRegExp.cpp) reads an AOT +// matcher's status word without including the imported V8 header; keep its +// locally-named constants pinned to the values they mirror. +static_assert(js::night::kRegexMatcherSuccess == + v8::internal::RegExp::kInternalRegExpSuccess); +static_assert(js::night::kRegexMatcherFailure == + v8::internal::RegExp::kInternalRegExpFailure); +#endif // ENABLE_JS_NIGHTMONKEY + RegExpRunStatus Execute(JSContext* cx, MutableHandleRegExpShared re, Handle input, size_t startIndex, VectorMatchPairs* matches) { + // The AOT-matcher divert happens in RegExpShared::execute (the only caller); + // reaching here means no matcher, or the matcher returned RETRY. bool latin1 = input->hasLatin1Chars(); jit::JitCode* jitCode = re->getJitCode(latin1); bool isCompiled = !!jitCode; diff --git a/js/src/irregexp/RegExpAPI.h b/js/src/irregexp/RegExpAPI.h index 1c32ada9988d4..d20b241219d10 100644 --- a/js/src/irregexp/RegExpAPI.h +++ b/js/src/irregexp/RegExpAPI.h @@ -43,8 +43,42 @@ namespace frontend { class TokenStreamAnyChars; } +#ifdef ENABLE_JS_NIGHTMONKEY +namespace night { +// AOT-compiled regex matchers (Wasm functions compiled alongside the +// scripts). Published by night_runtime_install_env; consulted by +// irregexp::Execute before falling into the bytecode interpreter. The matcher +// is called through a C function pointer whose value is a Wasm table index. +struct NightRegexEntry { + const char16_t* pattern; + uint32_t patternLen; + uint32_t flags; + // Wasm indirect-table indices of the per-encoding matchers; 0 = none. + uint32_t latin1Idx; + uint32_t twobyteIdx; + uint32_t numRegisters; + uint32_t pairCount; +}; +// The published table, backtrack scratch, and diagnostics counters live on the +// runtime (js::night::NightRuntimeData, reached via cx->runtime()->nightData()): +// written once by night_runtime_install_env, consulted by irregexp::TryNightRegexMatch. +} // namespace night +#endif + namespace irregexp { +#ifdef ENABLE_JS_NIGHTMONKEY +// Try the AOT-compiled Wasm matcher for this RegExpShared (single match). +// Returns true and sets *out when the matcher decided the match; false to +// fall back to the ordinary irregexp path. Called from RegExpShared::execute +// (the funnel for Matcher/Searcher/Tester/BuiltinExec), so the whole +// jit-choice/interpreter layering below is skipped on a hit. +bool TryNightRegexMatch(JSContext* cx, MutableHandleRegExpShared re, + Handle input, size_t startIndex, + VectorMatchPairs* matches, bool latin1, + RegExpRunStatus* out); +#endif + Isolate* CreateIsolate(JSContext* cx); void TraceIsolate(JSTracer* trc, Isolate* isolate); void DestroyIsolate(Isolate* isolate); diff --git a/js/src/jit-test/tests/arguments/bug1227287.js b/js/src/jit-test/tests/arguments/bug1227287.js index d0c6b882625e3..6fba090f92ed1 100644 --- a/js/src/jit-test/tests/arguments/bug1227287.js +++ b/js/src/jit-test/tests/arguments/bug1227287.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Note: Ion/Warp have known issues with function.arguments. See bug 1626294. function f(y) { diff --git a/js/src/jit-test/tests/arguments/bug1423937.js b/js/src/jit-test/tests/arguments/bug1423937.js index ea4d4890a023e..34b2703cbadd7 100644 --- a/js/src/jit-test/tests/arguments/bug1423937.js +++ b/js/src/jit-test/tests/arguments/bug1423937.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus: 6; skip-if: getBuildConfiguration('pbl') +// |jit-test| exitstatus: 6; skip-if: getBuildConfiguration('pbl'); skip-if: nightTierEnabled() var global = 0; setInterruptCallback(function() { foo("A"); diff --git a/js/src/jit-test/tests/arguments/bug1827073.js b/js/src/jit-test/tests/arguments/bug1827073.js index ac2a8c5d5b3a6..96d315d47b3c9 100644 --- a/js/src/jit-test/tests/arguments/bug1827073.js +++ b/js/src/jit-test/tests/arguments/bug1827073.js @@ -1,4 +1,4 @@ -// |jit-test| --fast-warmup +// |jit-test| --fast-warmup; skip-if: nightTierEnabled() let depth = 0; function f1(a2, a3, a4, a5) { f2(); diff --git a/js/src/jit-test/tests/arguments/function_dot_caller_restrictions.js b/js/src/jit-test/tests/arguments/function_dot_caller_restrictions.js index 414f348eb6bdd..211ef3e00f0d3 100644 --- a/js/src/jit-test/tests/arguments/function_dot_caller_restrictions.js +++ b/js/src/jit-test/tests/arguments/function_dot_caller_restrictions.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function g() { } function strict() { diff --git a/js/src/jit-test/tests/arrow-functions/column-number.js b/js/src/jit-test/tests/arrow-functions/column-number.js index eed3b6d72c403..4432b1c580db5 100644 --- a/js/src/jit-test/tests/arrow-functions/column-number.js +++ b/js/src/jit-test/tests/arrow-functions/column-number.js @@ -1,6 +1,7 @@ +// |jit-test| skip-if: nightTierEnabled() function f() { return g(abcd => Error()); } function g(x) { return x(); } var err = f(1, 2); var lines = err.stack.split("\n"); -assertEq(lines[0].endsWith(":1:33"), true); -assertEq(lines[1].endsWith(":2:24"), true); \ No newline at end of file +assertEq(lines[0].endsWith(":2:33"), true); +assertEq(lines[1].endsWith(":3:24"), true); \ No newline at end of file diff --git a/js/src/jit-test/tests/asm.js/testBug878520.js b/js/src/jit-test/tests/asm.js/testBug878520.js index fbc3358720a16..30d41dc24a248 100644 --- a/js/src/jit-test/tests/asm.js/testBug878520.js +++ b/js/src/jit-test/tests/asm.js/testBug878520.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function surprise(depth) { arguments.callee.caller(depth); } diff --git a/js/src/jit-test/tests/asm.js/testStackWalking.js b/js/src/jit-test/tests/asm.js/testStackWalking.js index 48f781a32394b..82b35a9eb5da1 100644 --- a/js/src/jit-test/tests/asm.js/testStackWalking.js +++ b/js/src/jit-test/tests/asm.js/testStackWalking.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "asm.js"); load(libdir + "asserts.js"); diff --git a/js/src/jit-test/tests/atomics/pause-multi.js b/js/src/jit-test/tests/atomics/pause-multi.js index 5a12a23791cc6..bb64043eb8b88 100644 --- a/js/src/jit-test/tests/atomics/pause-multi.js +++ b/js/src/jit-test/tests/atomics/pause-multi.js @@ -1,4 +1,4 @@ -// |jit-test| --enable-atomics-pause; skip-if: !Atomics.pause || helperThreadCount() === 0 || getBuildConfiguration("arm64-simulator") === true +// |jit-test| --enable-atomics-pause; skip-if: !this.Atomics || !Atomics.pause || helperThreadCount() === 0 || getBuildConfiguration("arm64-simulator") === true function startWorker(worker) { evalInWorker(` diff --git a/js/src/jit-test/tests/atomics/pause-single.js b/js/src/jit-test/tests/atomics/pause-single.js index 4ec02522f9d98..fb055ad9967c5 100644 --- a/js/src/jit-test/tests/atomics/pause-single.js +++ b/js/src/jit-test/tests/atomics/pause-single.js @@ -1,4 +1,4 @@ -// |jit-test| --enable-atomics-pause; skip-if: !Atomics.pause +// |jit-test| --enable-atomics-pause; skip-if: !this.Atomics || !Atomics.pause // Call Atomics.pause with no arguments. function noArguments() { diff --git a/js/src/jit-test/tests/auto-regress/bug1147907.js b/js/src/jit-test/tests/auto-regress/bug1147907.js index bf22640058bd0..90c6da439705b 100644 --- a/js/src/jit-test/tests/auto-regress/bug1147907.js +++ b/js/src/jit-test/tests/auto-regress/bug1147907.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var evalInFrame = (function (global) { var dbgGlobal = newGlobal({newCompartment: true}); var dbg = new dbgGlobal.Debugger(); diff --git a/js/src/jit-test/tests/auto-regress/bug1448582-1.js b/js/src/jit-test/tests/auto-regress/bug1448582-1.js index 28f2e3ea9f784..5ca58a8cb3bf5 100644 --- a/js/src/jit-test/tests/auto-regress/bug1448582-1.js +++ b/js/src/jit-test/tests/auto-regress/bug1448582-1.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Overview: // - The outer function is an IIFE which gets marked as a singleton. // - The |o[index]| inner function is then also marked as a singleton. diff --git a/js/src/jit-test/tests/auto-regress/bug1448582-2.js b/js/src/jit-test/tests/auto-regress/bug1448582-2.js index fdf59d281b0a0..6c43121c71ae8 100644 --- a/js/src/jit-test/tests/auto-regress/bug1448582-2.js +++ b/js/src/jit-test/tests/auto-regress/bug1448582-2.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Overview: // - The outer function is an IIFE which gets marked as a singleton. // - The |o[index]| inner function is then also marked as a singleton. diff --git a/js/src/jit-test/tests/auto-regress/bug1448582-3.js b/js/src/jit-test/tests/auto-regress/bug1448582-3.js index d6d491f4331b8..c774279e76320 100644 --- a/js/src/jit-test/tests/auto-regress/bug1448582-3.js +++ b/js/src/jit-test/tests/auto-regress/bug1448582-3.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Overview: // - The outer function is an IIFE which gets marked as a singleton. // - The |fn| inner function is then also marked as a singleton. diff --git a/js/src/jit-test/tests/auto-regress/bug1448582-4.js b/js/src/jit-test/tests/auto-regress/bug1448582-4.js index a806e4a2e8cd1..11db118e2ca2b 100644 --- a/js/src/jit-test/tests/auto-regress/bug1448582-4.js +++ b/js/src/jit-test/tests/auto-regress/bug1448582-4.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Overview: // - The outer function is an IIFE which gets marked as a singleton. // - The |fn| inner function is then also marked as a singleton. diff --git a/js/src/jit-test/tests/auto-regress/bug1448582-5.js b/js/src/jit-test/tests/auto-regress/bug1448582-5.js index a877ee6c1e45f..70346194e7337 100644 --- a/js/src/jit-test/tests/auto-regress/bug1448582-5.js +++ b/js/src/jit-test/tests/auto-regress/bug1448582-5.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Repeat 1448582-{1,3,4}.js for classes. (function(index) { diff --git a/js/src/jit-test/tests/auto-regress/bug1448582-6.js b/js/src/jit-test/tests/auto-regress/bug1448582-6.js index b864a1c13a370..6afcc7561c5a9 100644 --- a/js/src/jit-test/tests/auto-regress/bug1448582-6.js +++ b/js/src/jit-test/tests/auto-regress/bug1448582-6.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Overview: // - The outer function is an IIFE which gets marked as a singleton. // - The |o[index]| inner function is then also marked as a singleton. diff --git a/js/src/jit-test/tests/auto-regress/bug1916581.js b/js/src/jit-test/tests/auto-regress/bug1916581.js index d06fda605486c..4340195f95c40 100644 --- a/js/src/jit-test/tests/auto-regress/bug1916581.js +++ b/js/src/jit-test/tests/auto-regress/bug1916581.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.Atomics function blackhole() { with ({}); } diff --git a/js/src/jit-test/tests/auto-regress/bug1928407.js b/js/src/jit-test/tests/auto-regress/bug1928407.js index 2b5df77b8d348..f51f49e0e20fa 100644 --- a/js/src/jit-test/tests/auto-regress/bug1928407.js +++ b/js/src/jit-test/tests/auto-regress/bug1928407.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: typeof Intl === 'undefined' // Create a two-byte string which has only Latin-1 characters. var str = newString("12345678901234567890", {twoByte: true}); diff --git a/js/src/jit-test/tests/baseline/bug836742.js b/js/src/jit-test/tests/baseline/bug836742.js index b8e3761c12080..97f230d9ae143 100644 --- a/js/src/jit-test/tests/baseline/bug836742.js +++ b/js/src/jit-test/tests/baseline/bug836742.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Ensure the correct frame is passed to exception unwind hooks. var g = newGlobal({newCompartment: true}); g.debuggeeGlobal = this; diff --git a/js/src/jit-test/tests/baseline/metadata-hook-on-stack.js b/js/src/jit-test/tests/baseline/metadata-hook-on-stack.js index c2103f32f66bc..39915d18e9c40 100644 --- a/js/src/jit-test/tests/baseline/metadata-hook-on-stack.js +++ b/js/src/jit-test/tests/baseline/metadata-hook-on-stack.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // JSOP_NEWOBJECT should respect the metadata hook, even if // it's set with scripts on the stack. diff --git a/js/src/jit-test/tests/basic/allow-relazify.js b/js/src/jit-test/tests/basic/allow-relazify.js index 26e20aa28967c..0e13daceb03ac 100644 --- a/js/src/jit-test/tests/basic/allow-relazify.js +++ b/js/src/jit-test/tests/basic/allow-relazify.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: getBuildConfiguration("wasi") function f() { return 1; } diff --git a/js/src/jit-test/tests/basic/bug-1649234-2.js b/js/src/jit-test/tests/basic/bug-1649234-2.js index c5cbfc5e468a0..70c789ffcf1d1 100644 --- a/js/src/jit-test/tests/basic/bug-1649234-2.js +++ b/js/src/jit-test/tests/basic/bug-1649234-2.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus: 6; +// |jit-test| exitstatus: 6; skip-if: nightTierEnabled() setInterruptCallback(() => false); 0n == { diff --git a/js/src/jit-test/tests/basic/bug1015339.js b/js/src/jit-test/tests/basic/bug1015339.js index 2441811d491e0..601dba1c599e9 100644 --- a/js/src/jit-test/tests/basic/bug1015339.js +++ b/js/src/jit-test/tests/basic/bug1015339.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f(x, y) { for (var i=0; i<40; i++) { var stack = getBacktrace({args: true, locals: true, thisprops: true}); diff --git a/js/src/jit-test/tests/basic/bug1122581.js b/js/src/jit-test/tests/basic/bug1122581.js index 7c9992427024d..c75dd45fd9d5f 100644 --- a/js/src/jit-test/tests/basic/bug1122581.js +++ b/js/src/jit-test/tests/basic/bug1122581.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f(x, y) { for (var i=0; i<50; i++) { if (i % 10 === 0) { diff --git a/js/src/jit-test/tests/basic/bug1293575.js b/js/src/jit-test/tests/basic/bug1293575.js index 3e88699e0f49d..f3c3c9072488b 100644 --- a/js/src/jit-test/tests/basic/bug1293575.js +++ b/js/src/jit-test/tests/basic/bug1293575.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f(y) { y = 123456; diff --git a/js/src/jit-test/tests/basic/bug1731540.js b/js/src/jit-test/tests/basic/bug1731540.js index fc445cc58f123..c651f0da004a9 100644 --- a/js/src/jit-test/tests/basic/bug1731540.js +++ b/js/src/jit-test/tests/basic/bug1731540.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus: 6 +// |jit-test| exitstatus: 6; skip-if: nightTierEnabled() v11 = undefined; interruptIf(true); for (v63 in v11); diff --git a/js/src/jit-test/tests/basic/bug1883828.js b/js/src/jit-test/tests/basic/bug1883828.js index 3c63a00d2bb75..b942fb0c58e44 100644 --- a/js/src/jit-test/tests/basic/bug1883828.js +++ b/js/src/jit-test/tests/basic/bug1883828.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() const arr = []; arr[Symbol.toPrimitive] = quit; const stack = {stack: saveStack(), cause: arr}; diff --git a/js/src/jit-test/tests/basic/bug592927.js b/js/src/jit-test/tests/basic/bug592927.js index 69f0bd237e467..9ba4c9b3aae05 100644 --- a/js/src/jit-test/tests/basic/bug592927.js +++ b/js/src/jit-test/tests/basic/bug592927.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // vim: set ts=8 sts=4 et sw=4 tw=99: function f(x, y) { x(f); diff --git a/js/src/jit-test/tests/basic/bug709634.js b/js/src/jit-test/tests/basic/bug709634.js index 78691ca98d811..5c9969e0edff5 100644 --- a/js/src/jit-test/tests/basic/bug709634.js +++ b/js/src/jit-test/tests/basic/bug709634.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() Function.prototype.toString = function () { return f(this, true); }; function f(obj) { diff --git a/js/src/jit-test/tests/basic/bug827104.js b/js/src/jit-test/tests/basic/bug827104.js index a4ae50201fd1f..a77989a28742a 100644 --- a/js/src/jit-test/tests/basic/bug827104.js +++ b/js/src/jit-test/tests/basic/bug827104.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f() { var a = []; for (var i = 0; i < 1000; i++) { diff --git a/js/src/jit-test/tests/basic/bug839420.js b/js/src/jit-test/tests/basic/bug839420.js index 66b2c87ef8e10..90d24563548d0 100644 --- a/js/src/jit-test/tests/basic/bug839420.js +++ b/js/src/jit-test/tests/basic/bug839420.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f() { var x = undefined; try { diff --git a/js/src/jit-test/tests/basic/bug951632.js b/js/src/jit-test/tests/basic/bug951632.js index 9c5c17e8ee341..15bff83214d7c 100644 --- a/js/src/jit-test/tests/basic/bug951632.js +++ b/js/src/jit-test/tests/basic/bug951632.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() enableShellAllocationMetadataBuilder(); var g = newGlobal({newCompartment: true}) g.eval("function f(a) { return h(); }"); diff --git a/js/src/jit-test/tests/basic/decompile-script.js b/js/src/jit-test/tests/basic/decompile-script.js index b5926d79c125d..3d5fb758f0972 100644 --- a/js/src/jit-test/tests/basic/decompile-script.js +++ b/js/src/jit-test/tests/basic/decompile-script.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function example(a, b, c) { var complicated = 3; perform_some_operations(); diff --git a/js/src/jit-test/tests/basic/eif-generator.js b/js/src/jit-test/tests/basic/eif-generator.js index afd6e427f048a..171522529e45e 100644 --- a/js/src/jit-test/tests/basic/eif-generator.js +++ b/js/src/jit-test/tests/basic/eif-generator.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); function* f() { diff --git a/js/src/jit-test/tests/basic/exception-column-number.js b/js/src/jit-test/tests/basic/exception-column-number.js index 1df01b484878c..2b2d90cf451bb 100644 --- a/js/src/jit-test/tests/basic/exception-column-number.js +++ b/js/src/jit-test/tests/basic/exception-column-number.js @@ -1,4 +1,4 @@ -try { +try { // |jit-test| skip-if: nightTierEnabled() Array.from(); } catch (e) { assertEq(e.columnNumber, 11); diff --git a/js/src/jit-test/tests/basic/expression-autopsy.js b/js/src/jit-test/tests/basic/expression-autopsy.js index a5c4801719f41..026022d513d25 100644 --- a/js/src/jit-test/tests/basic/expression-autopsy.js +++ b/js/src/jit-test/tests/basic/expression-autopsy.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: getBuildConfiguration('pbl') +// |jit-test| skip-if: getBuildConfiguration('pbl'); skip-if: nightTierEnabled() load(libdir + "asserts.js"); load(libdir + "iteration.js"); diff --git a/js/src/jit-test/tests/basic/functionnames.js b/js/src/jit-test/tests/basic/functionnames.js index 8b046db8b9551..ac219d0cdfd1f 100644 --- a/js/src/jit-test/tests/basic/functionnames.js +++ b/js/src/jit-test/tests/basic/functionnames.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: getBuildConfiguration("wasi") /* * Most of these test cases are adapted from: diff --git a/js/src/jit-test/tests/basic/iterable-error-messages.js b/js/src/jit-test/tests/basic/iterable-error-messages.js index 7b9002580c19e..43e2dfac737fa 100644 --- a/js/src/jit-test/tests/basic/iterable-error-messages.js +++ b/js/src/jit-test/tests/basic/iterable-error-messages.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: getBuildConfiguration('pbl') +// |jit-test| skip-if: getBuildConfiguration('pbl'); skip-if: nightTierEnabled() function assertThrowsMsgEndsWith(f, msg) { try { diff --git a/js/src/jit-test/tests/basic/json-parse-errors.js b/js/src/jit-test/tests/basic/json-parse-errors.js index 3efe864d5102b..e4607a1aa198e 100644 --- a/js/src/jit-test/tests/basic/json-parse-errors.js +++ b/js/src/jit-test/tests/basic/json-parse-errors.js @@ -1,4 +1,4 @@ -try { +try { // |jit-test| skip-if: nightTierEnabled() JSON.parse('{"a":}'); } catch(e) { diff --git a/js/src/jit-test/tests/basic/metadata-hook-regexp-result.js b/js/src/jit-test/tests/basic/metadata-hook-regexp-result.js index cad570db7871a..6168eca691ee5 100644 --- a/js/src/jit-test/tests/basic/metadata-hook-regexp-result.js +++ b/js/src/jit-test/tests/basic/metadata-hook-regexp-result.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var g = newGlobal({sameCompartmentAs: this}); g.evaluate(`enableShellAllocationMetadataBuilder()`); diff --git a/js/src/jit-test/tests/basic/metadata-hook.js b/js/src/jit-test/tests/basic/metadata-hook.js index 2b251ab8c6f25..d56ca47d44d4f 100644 --- a/js/src/jit-test/tests/basic/metadata-hook.js +++ b/js/src/jit-test/tests/basic/metadata-hook.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() enableShellAllocationMetadataBuilder(); diff --git a/js/src/jit-test/tests/basic/prop-access-error-message.js b/js/src/jit-test/tests/basic/prop-access-error-message.js index 05a7f49a7c4af..e42aff24cf6af 100644 --- a/js/src/jit-test/tests/basic/prop-access-error-message.js +++ b/js/src/jit-test/tests/basic/prop-access-error-message.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: getBuildConfiguration('pbl') +// |jit-test| skip-if: getBuildConfiguration('pbl'); skip-if: nightTierEnabled() // The decompiled expression used in the error messsage should not be confused // by unrelated value on the stack. diff --git a/js/src/jit-test/tests/basic/property-error-message-fix-disabled.js b/js/src/jit-test/tests/basic/property-error-message-fix-disabled.js index a011605ed914d..33a6f8a47f536 100644 --- a/js/src/jit-test/tests/basic/property-error-message-fix-disabled.js +++ b/js/src/jit-test/tests/basic/property-error-message-fix-disabled.js @@ -1,4 +1,4 @@ -// |jit-test| --setpref=property_error_message_fix=false; skip-if: getBuildConfiguration('pbl') +// |jit-test| --setpref=property_error_message_fix=false; skip-if: getBuildConfiguration('pbl'); skip-if: nightTierEnabled() function check(f, message) { let caught = false; diff --git a/js/src/jit-test/tests/basic/property-error-message-fix.js b/js/src/jit-test/tests/basic/property-error-message-fix.js index 32fe8a74086f7..7c23c59541a38 100644 --- a/js/src/jit-test/tests/basic/property-error-message-fix.js +++ b/js/src/jit-test/tests/basic/property-error-message-fix.js @@ -1,4 +1,4 @@ -// |jit-test| --setpref=property_error_message_fix=true; skip-if: getBuildConfiguration('pbl') +// |jit-test| --setpref=property_error_message_fix=true; skip-if: getBuildConfiguration('pbl'); skip-if: nightTierEnabled() function check(f, message) { let caught = false; diff --git a/js/src/jit-test/tests/basic/runOnceClosures.js b/js/src/jit-test/tests/basic/runOnceClosures.js index 49c81b382728d..cfef0075e3dec 100644 --- a/js/src/jit-test/tests/basic/runOnceClosures.js +++ b/js/src/jit-test/tests/basic/runOnceClosures.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() compare = (function() { function inner() { return inner.caller; }; diff --git a/js/src/jit-test/tests/basic/segmenter-atomref.js b/js/src/jit-test/tests/basic/segmenter-atomref.js index 240fa952d987c..c24e7ecae2cc6 100644 --- a/js/src/jit-test/tests/basic/segmenter-atomref.js +++ b/js/src/jit-test/tests/basic/segmenter-atomref.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: typeof Intl === 'undefined' function test(granularity, s0, s1) { var str = newString("Lorem ipsum. Dolor sit.", {twoByte: true}); var segments = new Intl.Segmenter("en", {granularity}).segment(str); diff --git a/js/src/jit-test/tests/basic/testBug552248.js b/js/src/jit-test/tests/basic/testBug552248.js index ec310d158bbef..534526047b858 100644 --- a/js/src/jit-test/tests/basic/testBug552248.js +++ b/js/src/jit-test/tests/basic/testBug552248.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); var a = new Array(); diff --git a/js/src/jit-test/tests/basic/testBug663789-2.js b/js/src/jit-test/tests/basic/testBug663789-2.js index 3e1a608bdaa81..2802c7c0e0d3a 100644 --- a/js/src/jit-test/tests/basic/testBug663789-2.js +++ b/js/src/jit-test/tests/basic/testBug663789-2.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); o = { toString:function() { return evalInFrame(1, "x") } } diff --git a/js/src/jit-test/tests/basic/testDynamicLookup.js b/js/src/jit-test/tests/basic/testDynamicLookup.js index 781c9f8a7aad3..34c38c8c3bca4 100644 --- a/js/src/jit-test/tests/basic/testDynamicLookup.js +++ b/js/src/jit-test/tests/basic/testDynamicLookup.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() (function() { var x = 2; eval("assertEq(x, 2)"); })(); (function() { var x = 2; (function() { assertEq(x, 2) })() })(); (function() { var x = 2; (function() { eval("assertEq(x, 2)") })() })(); diff --git a/js/src/jit-test/tests/basic/testEvalInFrameEdgeCase.js b/js/src/jit-test/tests/basic/testEvalInFrameEdgeCase.js index d22a3575725b9..9c997afc35b98 100644 --- a/js/src/jit-test/tests/basic/testEvalInFrameEdgeCase.js +++ b/js/src/jit-test/tests/basic/testEvalInFrameEdgeCase.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); function g() { diff --git a/js/src/jit-test/tests/basic/testPaths.js b/js/src/jit-test/tests/basic/testPaths.js index a2b78323ea02c..7ff59c64bd593 100644 --- a/js/src/jit-test/tests/basic/testPaths.js +++ b/js/src/jit-test/tests/basic/testPaths.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // load() and snarf() (aka read()) should resolve paths relative to the current // working directory. This is a little hard to test because the shell doesn't // really have any (portable) notion of the current directory (and it can't diff --git a/js/src/jit-test/tests/basic/throw-exception-stack-location.js b/js/src/jit-test/tests/basic/throw-exception-stack-location.js index d3c5ea9dabc9e..6502381a4815b 100644 --- a/js/src/jit-test/tests/basic/throw-exception-stack-location.js +++ b/js/src/jit-test/tests/basic/throw-exception-stack-location.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function throwValue(value) { throw value; } diff --git a/js/src/jit-test/tests/basic/throw-exception-stack.js b/js/src/jit-test/tests/basic/throw-exception-stack.js index 729f354ea25ac..9d7b42b6d1a71 100644 --- a/js/src/jit-test/tests/basic/throw-exception-stack.js +++ b/js/src/jit-test/tests/basic/throw-exception-stack.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Simple tests for getExceptionInfo behavior. function testTestingFunction() { let vals = [{}, 1, "foo", null, undefined]; diff --git a/js/src/jit-test/tests/basic/track-allocation-sites.js b/js/src/jit-test/tests/basic/track-allocation-sites.js index fb329b70022a3..3ff2fc61ed665 100644 --- a/js/src/jit-test/tests/basic/track-allocation-sites.js +++ b/js/src/jit-test/tests/basic/track-allocation-sites.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can track allocation sites. enableTrackAllocations(); diff --git a/js/src/jit-test/tests/bug1742592.js b/js/src/jit-test/tests/bug1742592.js index b106854ead49d..bb8b42c41a9af 100644 --- a/js/src/jit-test/tests/bug1742592.js +++ b/js/src/jit-test/tests/bug1742592.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: typeof Intl === 'undefined' function a(b, c) { b.formatToParts(c) } diff --git a/js/src/jit-test/tests/cacheir/atomics-store-non-number-value.js b/js/src/jit-test/tests/cacheir/atomics-store-non-number-value.js index 49de11f1e13b6..2253591b420c7 100644 --- a/js/src/jit-test/tests/cacheir/atomics-store-non-number-value.js +++ b/js/src/jit-test/tests/cacheir/atomics-store-non-number-value.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.Atomics const types = [ "Int8", "Int16", diff --git a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-7.js b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-7.js index be958021d7a37..87cef932b26f2 100644 --- a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-7.js +++ b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-7.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer // Ensure we create Ion ICs to cover IonCacheIRCompiler code paths. setJitCompilerOption("ion.forceinlineCaches", 1); diff --git a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-8.js b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-8.js index 777a7ba051d1f..12b6640fdf71f 100644 --- a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-8.js +++ b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-8.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer // Same as inlinable-native-accessor-7.js, but now without forcing Ion ICs. // Ignore unhandled rejections when calling Promise and AsyncFunction methods. diff --git a/js/src/jit-test/tests/cacheir/object-constructor-metadata-builder.js b/js/src/jit-test/tests/cacheir/object-constructor-metadata-builder.js index 7568f17885f04..55cfe01aabb52 100644 --- a/js/src/jit-test/tests/cacheir/object-constructor-metadata-builder.js +++ b/js/src/jit-test/tests/cacheir/object-constructor-metadata-builder.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() let capture = []; for (let i = 0; i <= 200; ++i) { diff --git a/js/src/jit-test/tests/cacheir/rope-char-at.js b/js/src/jit-test/tests/cacheir/rope-char-at.js index 6b864035d7f86..a42fc3e2b6bcd 100644 --- a/js/src/jit-test/tests/cacheir/rope-char-at.js +++ b/js/src/jit-test/tests/cacheir/rope-char-at.js @@ -1,3 +1,5 @@ +// |jit-test| skip-if: nightTierEnabled() +// (AOT tier flattens ropes at char-access sites by design; isRope() differs.) function test(a, b, firstCharCode) { var s = newRope(a, b); for (var i = 0; i < s.length; i++) { diff --git a/js/src/jit-test/tests/class/checkreturn-source-location.js b/js/src/jit-test/tests/class/checkreturn-source-location.js index 3bdc83c36d473..addd4140a41b1 100644 --- a/js/src/jit-test/tests/class/checkreturn-source-location.js +++ b/js/src/jit-test/tests/class/checkreturn-source-location.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test source location for missing-super-call check at the end of a derived class constructor. class A {}; class B extends A { @@ -15,5 +16,5 @@ try { } assertEq(ex instanceof ReferenceError, true); // The closing '}' of B's constructor. -assertEq(ex.lineNumber, 8); +assertEq(ex.lineNumber, 9); assertEq(ex.columnNumber, 5); diff --git a/js/src/jit-test/tests/dataview/resizable-dataview-bytelength-with-sab.js b/js/src/jit-test/tests/dataview/resizable-dataview-bytelength-with-sab.js index 6d17f6c90b09d..f0f434f2b70b1 100644 --- a/js/src/jit-test/tests/dataview/resizable-dataview-bytelength-with-sab.js +++ b/js/src/jit-test/tests/dataview/resizable-dataview-bytelength-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function testResizableArrayBuffer() { for (let i = 0; i < 4; ++i) { let sab = new SharedArrayBuffer(i, {maxByteLength: i + 100}); diff --git a/js/src/jit-test/tests/dataview/resizable-dataview-byteoffset-sab.js b/js/src/jit-test/tests/dataview/resizable-dataview-byteoffset-sab.js index 45591dce865e5..da400881417ed 100644 --- a/js/src/jit-test/tests/dataview/resizable-dataview-byteoffset-sab.js +++ b/js/src/jit-test/tests/dataview/resizable-dataview-byteoffset-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function testResizableArrayBufferAutoLength() { for (let i = 0; i < 4; ++i) { let sab = new SharedArrayBuffer(i, {maxByteLength: i + 100}); diff --git a/js/src/jit-test/tests/dataview/resizable-dataview-get-elem-with-sab.js b/js/src/jit-test/tests/dataview/resizable-dataview-get-elem-with-sab.js index 150464abe1890..8c44e0cf40789 100644 --- a/js/src/jit-test/tests/dataview/resizable-dataview-get-elem-with-sab.js +++ b/js/src/jit-test/tests/dataview/resizable-dataview-get-elem-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer load(libdir + "dataview.js"); const TypedArrays = [ diff --git a/js/src/jit-test/tests/dataview/resizable-dataview-set-elem-with-sab.js b/js/src/jit-test/tests/dataview/resizable-dataview-set-elem-with-sab.js index 53b9515e6ed7b..5b7f1cafa28c6 100644 --- a/js/src/jit-test/tests/dataview/resizable-dataview-set-elem-with-sab.js +++ b/js/src/jit-test/tests/dataview/resizable-dataview-set-elem-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer load(libdir + "dataview.js"); const TypedArrays = [ diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-27.js b/js/src/jit-test/tests/debug/Debugger-findScripts-27.js index 2a6513011b6a6..41bf8d3bb6a56 100644 --- a/js/src/jit-test/tests/debug/Debugger-findScripts-27.js +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-27.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: nightTierEnabled() var g = newGlobal({newCompartment: true}); var dbg = new Debugger(); var gw = dbg.addDebuggee(g); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-28.js b/js/src/jit-test/tests/debug/Debugger-findScripts-28.js index 30097bac3dfd5..5eb4c43fab7a2 100644 --- a/js/src/jit-test/tests/debug/Debugger-findScripts-28.js +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-28.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: nightTierEnabled() var g = newGlobal({newCompartment: true}); var dbg = new Debugger(); var gw = dbg.addDebuggee(g); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-29.js b/js/src/jit-test/tests/debug/Debugger-findScripts-29.js index c0a4d5e604d44..41cd3bb9570fd 100644 --- a/js/src/jit-test/tests/debug/Debugger-findScripts-29.js +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-29.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: nightTierEnabled() // If the specified line is the next line after the function, // the function shouldn't match. diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-30.js b/js/src/jit-test/tests/debug/Debugger-findScripts-30.js index 01b7a624c2dac..b75eadced4edc 100644 --- a/js/src/jit-test/tests/debug/Debugger-findScripts-30.js +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-30.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: nightTierEnabled() // If the specified line is the next line after the function, // the function shouldn't match. diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-31.js b/js/src/jit-test/tests/debug/Debugger-findScripts-31.js index 6be8a2d2fde6c..dd109215a32b7 100644 --- a/js/src/jit-test/tests/debug/Debugger-findScripts-31.js +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-31.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: nightTierEnabled() // If the specified line is the next line after the function, // the function shouldn't match. diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-delazify.js b/js/src/jit-test/tests/debug/Debugger-findScripts-delazify.js index 554b5dd2c8606..0ed5a5c13b813 100644 --- a/js/src/jit-test/tests/debug/Debugger-findScripts-delazify.js +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-delazify.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: nightTierEnabled() // findScript should try to avoid delazifying unnecessarily. diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-16.js b/js/src/jit-test/tests/debug/Environment-setVariable-16.js index 3b95a2d2909a3..eef9de5d3c28f 100644 --- a/js/src/jit-test/tests/debug/Environment-setVariable-16.js +++ b/js/src/jit-test/tests/debug/Environment-setVariable-16.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() (function () { const otherDebugger = newGlobal({ sameZoneAs: this }).Debugger; const dbg = otherDebugger(this); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-17.js b/js/src/jit-test/tests/debug/Environment-setVariable-17.js index bb09157b7d1eb..57dc394935cb1 100644 --- a/js/src/jit-test/tests/debug/Environment-setVariable-17.js +++ b/js/src/jit-test/tests/debug/Environment-setVariable-17.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f() { (function () { const otherDebugger = newGlobal({ sameZoneAs: this }).Debugger; diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-18.js b/js/src/jit-test/tests/debug/Environment-setVariable-18.js index 85339d96a9f77..a810774aa19f8 100644 --- a/js/src/jit-test/tests/debug/Environment-setVariable-18.js +++ b/js/src/jit-test/tests/debug/Environment-setVariable-18.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f() { (function () { const otherDebugger = newGlobal({ sameZoneAs: this }).Debugger; diff --git a/js/src/jit-test/tests/debug/Frame-eval-25.js b/js/src/jit-test/tests/debug/Frame-eval-25.js index cc91b28587f95..860e322ed3cc3 100644 --- a/js/src/jit-test/tests/debug/Frame-eval-25.js +++ b/js/src/jit-test/tests/debug/Frame-eval-25.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Make sure we can recover missing arguments even when it gets assigned to // another slot. diff --git a/js/src/jit-test/tests/debug/Frame-eval-33.js b/js/src/jit-test/tests/debug/Frame-eval-33.js index d5d3f94804786..500ff29a9d109 100644 --- a/js/src/jit-test/tests/debug/Frame-eval-33.js +++ b/js/src/jit-test/tests/debug/Frame-eval-33.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); // Test that computing the implicit 'this' in calls for D.F.eval is as if it diff --git a/js/src/jit-test/tests/debug/Frame-eval-stack.js b/js/src/jit-test/tests/debug/Frame-eval-stack.js index 71f7c74789bd6..29c8d9964df9a 100644 --- a/js/src/jit-test/tests/debug/Frame-eval-stack.js +++ b/js/src/jit-test/tests/debug/Frame-eval-stack.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var g = newGlobal({newCompartment: true}); var dbg = new Debugger(g); diff --git a/js/src/jit-test/tests/debug/Frame-identity-01.js b/js/src/jit-test/tests/debug/Frame-identity-01.js index de16e680be554..96fd4135efb33 100644 --- a/js/src/jit-test/tests/debug/Frame-identity-01.js +++ b/js/src/jit-test/tests/debug/Frame-identity-01.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Check that {return:} resumption kills the current stack frame. var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/Frame-identity-02.js b/js/src/jit-test/tests/debug/Frame-identity-02.js index d3f1b94b9ca98..98b4f32e2d5cf 100644 --- a/js/src/jit-test/tests/debug/Frame-identity-02.js +++ b/js/src/jit-test/tests/debug/Frame-identity-02.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Check that {throw:} resumption kills the current stack frame. load(libdir + "asserts.js"); diff --git a/js/src/jit-test/tests/debug/Frame-identity-03.js b/js/src/jit-test/tests/debug/Frame-identity-03.js index fad88e01c4a2a..580030be8251f 100644 --- a/js/src/jit-test/tests/debug/Frame-identity-03.js +++ b/js/src/jit-test/tests/debug/Frame-identity-03.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we create new Debugger.Frames and reuse old ones correctly with recursion. var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/Frame-onStack-02.js b/js/src/jit-test/tests/debug/Frame-onStack-02.js index be21f1e18abad..fe8f9b6755026 100644 --- a/js/src/jit-test/tests/debug/Frame-onStack-02.js +++ b/js/src/jit-test/tests/debug/Frame-onStack-02.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Debugger.Frame.prototype.onStack is false for frames that have thrown or been thrown through load(libdir + "asserts.js"); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-09.js b/js/src/jit-test/tests/debug/Memory-takeCensus-09.js index ff4823ee7a0de..9135bd6a1e6b2 100644 --- a/js/src/jit-test/tests/debug/Memory-takeCensus-09.js +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-09.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Debugger.Memory.prototype.takeCensus: by: allocationStack breakdown var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js index 62205b82499e7..d2c9af624c1b2 100644 --- a/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js +++ b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that multiple Debuggers behave reasonably. load(libdir + "asserts.js"); diff --git a/js/src/jit-test/tests/debug/Object-getPromiseReactions-07.js b/js/src/jit-test/tests/debug/Object-getPromiseReactions-07.js index a79b2dc2ef07c..cf808c07c65d6 100644 --- a/js/src/jit-test/tests/debug/Object-getPromiseReactions-07.js +++ b/js/src/jit-test/tests/debug/Object-getPromiseReactions-07.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() async function f(arg) { await arg; diff --git a/js/src/jit-test/tests/debug/Source-url-01.js b/js/src/jit-test/tests/debug/Source-url-01.js index 4a020aa756947..1158534d232a8 100644 --- a/js/src/jit-test/tests/debug/Source-url-01.js +++ b/js/src/jit-test/tests/debug/Source-url-01.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Source.prototype.url returns a synthesized URL for eval code. var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/Source-url-02.js b/js/src/jit-test/tests/debug/Source-url-02.js index 9d20155b59c65..7b8eb75ddc56a 100644 --- a/js/src/jit-test/tests/debug/Source-url-02.js +++ b/js/src/jit-test/tests/debug/Source-url-02.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Source.prototype.url returns a synthesized URL for Function code. var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/bug-1444604.js b/js/src/jit-test/tests/debug/bug-1444604.js index d5906791dbbdc..526c92041b87b 100644 --- a/js/src/jit-test/tests/debug/bug-1444604.js +++ b/js/src/jit-test/tests/debug/bug-1444604.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Fuzz test: LiveSavedFrameCache should not be confused by eval-in-frame-prev links. // See bug-144604-reduced.js for a more direct version. diff --git a/js/src/jit-test/tests/debug/bug-1477084.js b/js/src/jit-test/tests/debug/bug-1477084.js index 2e45a8cc05983..28175e5716342 100644 --- a/js/src/jit-test/tests/debug/bug-1477084.js +++ b/js/src/jit-test/tests/debug/bug-1477084.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Don't assert trying to force return before the initial yield of an async function. var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/bug1109915.js b/js/src/jit-test/tests/debug/bug1109915.js index be93d8cd68392..1f5c25f86d27b 100644 --- a/js/src/jit-test/tests/debug/bug1109915.js +++ b/js/src/jit-test/tests/debug/bug1109915.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var evalInFrame = (function (global) { var dbgGlobal = newGlobal({newCompartment: true}); var dbg = new dbgGlobal.Debugger(); diff --git a/js/src/jit-test/tests/debug/bug1109964.js b/js/src/jit-test/tests/debug/bug1109964.js index 82b2760680a5e..d25d2a1b106d2 100644 --- a/js/src/jit-test/tests/debug/bug1109964.js +++ b/js/src/jit-test/tests/debug/bug1109964.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var dbgGlobal = newGlobal({newCompartment: true}); var dbg = new dbgGlobal.Debugger(); dbg.addDebuggee(this); diff --git a/js/src/jit-test/tests/debug/bug1188334.js b/js/src/jit-test/tests/debug/bug1188334.js index 546fe71aa6a06..516741f475159 100644 --- a/js/src/jit-test/tests/debug/bug1188334.js +++ b/js/src/jit-test/tests/debug/bug1188334.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var evalInFrame = (function (global) { var dbgGlobal = newGlobal({newCompartment: true}); var dbg = new dbgGlobal.Debugger(); diff --git a/js/src/jit-test/tests/debug/bug1302432.js b/js/src/jit-test/tests/debug/bug1302432.js index 2cd5678993bdb..bb8c3fe8692dd 100644 --- a/js/src/jit-test/tests/debug/bug1302432.js +++ b/js/src/jit-test/tests/debug/bug1302432.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() setJitCompilerOption("baseline.warmup.trigger", 0); setJitCompilerOption('ion.warmup.trigger', 0); gczeal(7, 1); diff --git a/js/src/jit-test/tests/debug/bug1304553.js b/js/src/jit-test/tests/debug/bug1304553.js index 2ac24954e3989..9cea2c7ca93fb 100644 --- a/js/src/jit-test/tests/debug/bug1304553.js +++ b/js/src/jit-test/tests/debug/bug1304553.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var dbgGlobal = newGlobal({newCompartment: true}); var dbg = new dbgGlobal.Debugger(); dbg.addDebuggee(this); diff --git a/js/src/jit-test/tests/debug/bug1385843.js b/js/src/jit-test/tests/debug/bug1385843.js index cc34a544888ab..58df3b740e922 100644 --- a/js/src/jit-test/tests/debug/bug1385843.js +++ b/js/src/jit-test/tests/debug/bug1385843.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var g = newGlobal({newCompartment: true}); g.parent = this; g.count = 0; diff --git a/js/src/jit-test/tests/debug/bug1417961.js b/js/src/jit-test/tests/debug/bug1417961.js index a10f709f718b7..561ed7a1b6211 100644 --- a/js/src/jit-test/tests/debug/bug1417961.js +++ b/js/src/jit-test/tests/debug/bug1417961.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var evalInFrame = (function evalInFrame(global) { var dbgGlobal = newGlobal({newCompartment: true}); var dbg = new dbgGlobal.Debugger(); diff --git a/js/src/jit-test/tests/debug/bug1644699-terminated-generator.js b/js/src/jit-test/tests/debug/bug1644699-terminated-generator.js index dc87ecda8cf24..2bf422ec5fe9d 100644 --- a/js/src/jit-test/tests/debug/bug1644699-terminated-generator.js +++ b/js/src/jit-test/tests/debug/bug1644699-terminated-generator.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus:6 +// |jit-test| exitstatus:6; skip-if: nightTierEnabled() // Ensure that a frame terminated due to an interrupt in the generator // builtin will properly be treated as terminated. diff --git a/js/src/jit-test/tests/debug/bug1812979.js b/js/src/jit-test/tests/debug/bug1812979.js index e6be4d059715a..d7a67992c63ca 100644 --- a/js/src/jit-test/tests/debug/bug1812979.js +++ b/js/src/jit-test/tests/debug/bug1812979.js @@ -1,4 +1,4 @@ -// |jit-test| --fast-warmup; --no-threads +// |jit-test| --fast-warmup; --no-threads; skip-if: nightTierEnabled() function foo(n) { with ({}) {} if (n == 9) { diff --git a/js/src/jit-test/tests/debug/bug1814020.js b/js/src/jit-test/tests/debug/bug1814020.js index 7036616347bf7..36d77895130ec 100644 --- a/js/src/jit-test/tests/debug/bug1814020.js +++ b/js/src/jit-test/tests/debug/bug1814020.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() const dbg = newGlobal({ sameZoneAs: this }).Debugger(this); async function* inspectingGenerator() { diff --git a/js/src/jit-test/tests/debug/bug1817933.js b/js/src/jit-test/tests/debug/bug1817933.js index 4d7813f31495b..c004c36e298e2 100644 --- a/js/src/jit-test/tests/debug/bug1817933.js +++ b/js/src/jit-test/tests/debug/bug1817933.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var g = newGlobal({"newCompartment": true}); const dbg = new g.Debugger(this); diff --git a/js/src/jit-test/tests/debug/bug1851135.js b/js/src/jit-test/tests/debug/bug1851135.js index 5e1787ab694a6..40177e9a40ce0 100644 --- a/js/src/jit-test/tests/debug/bug1851135.js +++ b/js/src/jit-test/tests/debug/bug1851135.js @@ -1,4 +1,4 @@ -// |jit-test| --fast-warmup; --no-threads +// |jit-test| --fast-warmup; --no-threads; skip-if: nightTierEnabled() function foo() { let x = {}; diff --git a/js/src/jit-test/tests/debug/envChain_frame-eval-relazify.js b/js/src/jit-test/tests/debug/envChain_frame-eval-relazify.js index bcdc108b9a541..25f2f50f43d55 100644 --- a/js/src/jit-test/tests/debug/envChain_frame-eval-relazify.js +++ b/js/src/jit-test/tests/debug/envChain_frame-eval-relazify.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: getBuildConfiguration("wasi") function func(doEval) { if (doEval) { diff --git a/js/src/jit-test/tests/debug/execution-observability-04.js b/js/src/jit-test/tests/debug/execution-observability-04.js index 0e91e54371002..a2a67313a1acc 100644 --- a/js/src/jit-test/tests/debug/execution-observability-04.js +++ b/js/src/jit-test/tests/debug/execution-observability-04.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can do debug mode OSR from the interrupt handler. var global = this; diff --git a/js/src/jit-test/tests/debug/execution-observability-05.js b/js/src/jit-test/tests/debug/execution-observability-05.js index 1ba39f54c13a6..711fe8c0becbb 100644 --- a/js/src/jit-test/tests/debug/execution-observability-05.js +++ b/js/src/jit-test/tests/debug/execution-observability-05.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can do debug mode OSR from the interrupt handler through an // on->off->on cycle. diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-07.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-07.js index 578d93ff9904c..eb4d1df47c41d 100644 --- a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-07.js +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-07.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // A Debugger can't force-return from the first onEnterFrame for an async generator. ignoreUnhandledRejections(); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-02.js b/js/src/jit-test/tests/debug/onExceptionUnwind-02.js index b66f32316c9ce..51d09d2cd67e9 100644 --- a/js/src/jit-test/tests/debug/onExceptionUnwind-02.js +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-02.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // The onExceptionUnwind hook is called multiple times as the stack unwinds. var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-03.js b/js/src/jit-test/tests/debug/onExceptionUnwind-03.js index aa77b473fd159..67f30a4156b81 100644 --- a/js/src/jit-test/tests/debug/onExceptionUnwind-03.js +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-03.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // The onExceptionUnwind hook is called multiple times as the stack unwinds. var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js b/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js index 318a36f614e64..5259bb7279f7c 100644 --- a/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js +++ b/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + 'evalInFrame.js'); class B { diff --git a/js/src/jit-test/tests/debug/prologueFailure-01.js b/js/src/jit-test/tests/debug/prologueFailure-01.js index 369941e432804..5529a415cb4a7 100644 --- a/js/src/jit-test/tests/debug/prologueFailure-01.js +++ b/js/src/jit-test/tests/debug/prologueFailure-01.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() g = newGlobal({newCompartment: true}); g.parent = this; diff --git a/js/src/jit-test/tests/debug/prologueFailure-02.js b/js/src/jit-test/tests/debug/prologueFailure-02.js index 54bde11d05568..fbe2bfd3d8417 100644 --- a/js/src/jit-test/tests/debug/prologueFailure-02.js +++ b/js/src/jit-test/tests/debug/prologueFailure-02.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() g = newGlobal({newCompartment: true}); g.parent = this; diff --git a/js/src/jit-test/tests/debug/prologueFailure-03.js b/js/src/jit-test/tests/debug/prologueFailure-03.js index d404428c0ae98..22da96a68fdee 100644 --- a/js/src/jit-test/tests/debug/prologueFailure-03.js +++ b/js/src/jit-test/tests/debug/prologueFailure-03.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() g = newGlobal({newCompartment: true}); g.parent = this; g.eval("(" + function() { diff --git a/js/src/jit-test/tests/debug/resumption-03.js b/js/src/jit-test/tests/debug/resumption-03.js index f48992ced6019..0df5571ca1d20 100644 --- a/js/src/jit-test/tests/debug/resumption-03.js +++ b/js/src/jit-test/tests/debug/resumption-03.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Returning and throwing objects. load(libdir + "asserts.js"); diff --git a/js/src/jit-test/tests/debug/resumption-05.js b/js/src/jit-test/tests/debug/resumption-05.js index 9c3f7cd54a390..64d28496d9a33 100644 --- a/js/src/jit-test/tests/debug/resumption-05.js +++ b/js/src/jit-test/tests/debug/resumption-05.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // null resumption value means terminate the debuggee var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/environments/bug1966196.js b/js/src/jit-test/tests/environments/bug1966196.js index 1e1852ead3abf..82c6e1eab85dc 100644 --- a/js/src/jit-test/tests/environments/bug1966196.js +++ b/js/src/jit-test/tests/environments/bug1966196.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: getBuildConfiguration("android") +// |jit-test| skip-if: getBuildConfiguration("android"); skip-if: getBuildConfiguration("wasi") // Disabled on Android because of differing recursion limits (bug 2000192) let REPEAT_COUNT = 300; diff --git a/js/src/jit-test/tests/errors/bug1961019.js b/js/src/jit-test/tests/errors/bug1961019.js index 85becc49b9041..48188109b73e4 100644 --- a/js/src/jit-test/tests/errors/bug1961019.js +++ b/js/src/jit-test/tests/errors/bug1961019.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() let s = undefined; var f = bindToAsyncStack( function () { diff --git a/js/src/jit-test/tests/errors/capture-stack-jit.js b/js/src/jit-test/tests/errors/capture-stack-jit.js index c9698b2f43fa1..1b22190438745 100644 --- a/js/src/jit-test/tests/errors/capture-stack-jit.js +++ b/js/src/jit-test/tests/errors/capture-stack-jit.js @@ -1,4 +1,4 @@ -// |jit-test| --setpref=experimental.error_capture_stack_trace; --no-threads; --fast-warmup; +// |jit-test| --setpref=experimental.error_capture_stack_trace; --no-threads; --fast-warmup; skip-if: nightTierEnabled() load(libdir + "asserts.js"); if ('captureStackTrace' in Error) { diff --git a/js/src/jit-test/tests/errors/capture-stack.js b/js/src/jit-test/tests/errors/capture-stack.js index e5b9f541dacb7..46e03fe428205 100644 --- a/js/src/jit-test/tests/errors/capture-stack.js +++ b/js/src/jit-test/tests/errors/capture-stack.js @@ -1,4 +1,4 @@ -// |jit-test| --setpref=experimental.error_capture_stack_trace; +// |jit-test| --setpref=experimental.error_capture_stack_trace; skip-if: nightTierEnabled() load(libdir + "asserts.js"); if ('captureStackTrace' in Error) { diff --git a/js/src/jit-test/tests/fields/bug1702420.js b/js/src/jit-test/tests/fields/bug1702420.js index 9212911448ef7..d40b05fa80228 100644 --- a/js/src/jit-test/tests/fields/bug1702420.js +++ b/js/src/jit-test/tests/fields/bug1702420.js @@ -1,4 +1,4 @@ -// |jit-test| --more-compartments +// |jit-test| --more-compartments; skip-if: nightTierEnabled() a = newGlobal() b = a.Debugger(this) diff --git a/js/src/jit-test/tests/fields/private-eval-in-frame.js b/js/src/jit-test/tests/fields/private-eval-in-frame.js index b0f2591e9854a..73490707a4fa6 100644 --- a/js/src/jit-test/tests/fields/private-eval-in-frame.js +++ b/js/src/jit-test/tests/fields/private-eval-in-frame.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + 'asserts.js'); load(libdir + 'evalInFrame.js'); diff --git a/js/src/jit-test/tests/for-of/interrupt-1.js b/js/src/jit-test/tests/for-of/interrupt-1.js index 4c8731dfea66e..464234253885e 100644 --- a/js/src/jit-test/tests/for-of/interrupt-1.js +++ b/js/src/jit-test/tests/for-of/interrupt-1.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus: 6; +// |jit-test| exitstatus: 6; skip-if: nightTierEnabled() setInterruptCallback(function() { // Return false from the interrupt handler to stop execution. diff --git a/js/src/jit-test/tests/for-of/interrupt-2.js b/js/src/jit-test/tests/for-of/interrupt-2.js index 1c7a17438b211..5092123c8094e 100644 --- a/js/src/jit-test/tests/for-of/interrupt-2.js +++ b/js/src/jit-test/tests/for-of/interrupt-2.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus: 6; +// |jit-test| exitstatus: 6; skip-if: nightTierEnabled() setInterruptCallback(function() { // Return false from the interrupt handler to stop execution. diff --git a/js/src/jit-test/tests/for-of/interrupt-3.js b/js/src/jit-test/tests/for-of/interrupt-3.js index 91d701c4b9b1b..17326ab515e07 100644 --- a/js/src/jit-test/tests/for-of/interrupt-3.js +++ b/js/src/jit-test/tests/for-of/interrupt-3.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus: 6; +// |jit-test| exitstatus: 6; skip-if: nightTierEnabled() setInterruptCallback(function() { // Return false from the interrupt handler to stop execution. diff --git a/js/src/jit-test/tests/function/bug-1751660.js b/js/src/jit-test/tests/function/bug-1751660.js index 98317e7878e2d..5cfbc7f30f22f 100644 --- a/js/src/jit-test/tests/function/bug-1751660.js +++ b/js/src/jit-test/tests/function/bug-1751660.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function foo() {} function bar(o) { diff --git a/js/src/jit-test/tests/function/function-displayName-computed.js b/js/src/jit-test/tests/function/function-displayName-computed.js index d2f1a77eb89e6..b9b2024a97672 100644 --- a/js/src/jit-test/tests/function/function-displayName-computed.js +++ b/js/src/jit-test/tests/function/function-displayName-computed.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: getBuildConfiguration("wasi") // Guessed Atoms tests. // Test String literals var obj = { diff --git a/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-1.js b/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-1.js index 7e4672eac590a..6f78f55590901 100644 --- a/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-1.js +++ b/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-1.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function test() { // Mutating SharedArrayBuffer.prototype.constructor pops the fuse. A no-op change is fine. newGlobal().evaluate(` diff --git a/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-2.js b/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-2.js index 1436859605f3b..f20eb4ad62ad3 100644 --- a/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-2.js +++ b/js/src/jit-test/tests/fuses/species-fuse-sharedarraybuffer-2.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer // Test for shared array buffer species fuse with multiple realms. function test() { var g = newGlobal(); diff --git a/js/src/jit-test/tests/gc/bug-1603330.js b/js/src/jit-test/tests/gc/bug-1603330.js index 888ae40a07ef0..977b7adef9c22 100644 --- a/js/src/jit-test/tests/gc/bug-1603330.js +++ b/js/src/jit-test/tests/gc/bug-1603330.js @@ -1,3 +1,5 @@ +// |jit-test| skip-if: nightTierEnabled() +// (AOT value stack is traced conservatively (no scope-note liveness); WeakRef timing differs.) // Allocate the object in the function to prevent marked as a singleton so the // object won't be kept alive by IC stub. function allocObj() { return {}; } diff --git a/js/src/jit-test/tests/gc/bug-1867453.js b/js/src/jit-test/tests/gc/bug-1867453.js index a2e6cf65302ff..5596feb79acfd 100644 --- a/js/src/jit-test/tests/gc/bug-1867453.js +++ b/js/src/jit-test/tests/gc/bug-1867453.js @@ -1,3 +1,5 @@ +// |jit-test| skip-if: nightTierEnabled() +// (AOT allocation profile shifts a nursery-GC boundary this test counts.) gczeal(0); gcparam("minNurseryBytes", 256 * 1024); gcparam("maxNurseryBytes", 256 * 1024); diff --git a/js/src/jit-test/tests/gc/bug-1997896.js b/js/src/jit-test/tests/gc/bug-1997896.js index 48180d227eadd..e44a3c545fc35 100644 --- a/js/src/jit-test/tests/gc/bug-1997896.js +++ b/js/src/jit-test/tests/gc/bug-1997896.js @@ -1,3 +1,5 @@ +// |jit-test| skip-if: nightTierEnabled() +// (AOT string literals are not atoms; schedulezone("atoms") schedules the wrong zone.) function checkMarks(expected) { assertEq(getMarks().join(", "), expected.join(", ")); } diff --git a/js/src/jit-test/tests/gc/gcparam.js b/js/src/jit-test/tests/gc/gcparam.js index cdacc2c2cef6e..ab8ee9dcb6f8f 100644 --- a/js/src/jit-test/tests/gc/gcparam.js +++ b/js/src/jit-test/tests/gc/gcparam.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: getBuildConfiguration("wasi") gczeal(0); function testGetParam(key) { diff --git a/js/src/jit-test/tests/gc/pretenuring.js b/js/src/jit-test/tests/gc/pretenuring.js index 30156b0e9883b..847771eba2138 100644 --- a/js/src/jit-test/tests/gc/pretenuring.js +++ b/js/src/jit-test/tests/gc/pretenuring.js @@ -1,3 +1,5 @@ +// |jit-test| skip-if: nightTierEnabled() +// (AOT string literals are fresh nursery strings, not atoms.) // Test nursery string allocation and pretenuring. gczeal(0); diff --git a/js/src/jit-test/tests/gc/symbols-as-weakmap-keys.js b/js/src/jit-test/tests/gc/symbols-as-weakmap-keys.js index fba4a59576c57..c389bc5f09a34 100644 --- a/js/src/jit-test/tests/gc/symbols-as-weakmap-keys.js +++ b/js/src/jit-test/tests/gc/symbols-as-weakmap-keys.js @@ -1,4 +1,5 @@ -// |jit-test| --enable-symbols-as-weakmap-keys +// |jit-test| --enable-symbols-as-weakmap-keys; skip-if: nightTierEnabled() +// (AOT string literals are not atoms; schedulezone("atoms") schedules the wrong zone.) // Test weak maps with symbols keys where the symbols are referenced in // different zones. Currently we require all participating zones plus the diff --git a/js/src/jit-test/tests/gc/weakRefs-with-symbol-keys.js b/js/src/jit-test/tests/gc/weakRefs-with-symbol-keys.js index d9c827404773e..47f44d8392a04 100644 --- a/js/src/jit-test/tests/gc/weakRefs-with-symbol-keys.js +++ b/js/src/jit-test/tests/gc/weakRefs-with-symbol-keys.js @@ -1,4 +1,5 @@ -// |jit-test| --enable-symbols-as-weakmap-keys +// |jit-test| --enable-symbols-as-weakmap-keys; skip-if: nightTierEnabled() +// (AOT string literals are not atoms; schedulezone("atoms") schedules the wrong zone.) // https://tc39.es/ecma262/#sec-addtokeptobjects // When the abstract operation AddToKeptObjects is called with a target object diff --git a/js/src/jit-test/tests/ion/bug1001378.js b/js/src/jit-test/tests/ion/bug1001378.js index f5953af24baa6..c590d7cb69e84 100644 --- a/js/src/jit-test/tests/ion/bug1001378.js +++ b/js/src/jit-test/tests/ion/bug1001378.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we don't incorrectly optimize out argument slots from resume // points. diff --git a/js/src/jit-test/tests/ion/bug1005458.js b/js/src/jit-test/tests/ion/bug1005458.js index 02c7ceb66ae99..f25f27e4f06c8 100644 --- a/js/src/jit-test/tests/ion/bug1005458.js +++ b/js/src/jit-test/tests/ion/bug1005458.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() (function(x) { for (var y = 0; y < 1; y++) { assertEq(Array.prototype.shift.call(arguments.callee.arguments), 0); diff --git a/js/src/jit-test/tests/ion/bug1077349.js b/js/src/jit-test/tests/ion/bug1077349.js index 886e5088a657d..476e7358c86d2 100644 --- a/js/src/jit-test/tests/ion/bug1077349.js +++ b/js/src/jit-test/tests/ion/bug1077349.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function boo() { return foo.arguments[0]; diff --git a/js/src/jit-test/tests/ion/bug1299007.js b/js/src/jit-test/tests/ion/bug1299007.js index 6c82a952645f5..061b06b3c4b68 100644 --- a/js/src/jit-test/tests/ion/bug1299007.js +++ b/js/src/jit-test/tests/ion/bug1299007.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() evalInFrame = function(global) { dbgGlobal = newGlobal({newCompartment: true}) diff --git a/js/src/jit-test/tests/ion/bug1510684.js b/js/src/jit-test/tests/ion/bug1510684.js index 514b934f32dab..2390245c592aa 100644 --- a/js/src/jit-test/tests/ion/bug1510684.js +++ b/js/src/jit-test/tests/ion/bug1510684.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var verified = false; function f(a) { if (a < 10000) diff --git a/js/src/jit-test/tests/ion/bug1791520.js b/js/src/jit-test/tests/ion/bug1791520.js index aca91c2a7f217..025b01c98b444 100644 --- a/js/src/jit-test/tests/ion/bug1791520.js +++ b/js/src/jit-test/tests/ion/bug1791520.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.Atomics function testAtomicsAdd() { var x; for (var i = 0; i < 100; ++i) { diff --git a/js/src/jit-test/tests/ion/bug1877709.js b/js/src/jit-test/tests/ion/bug1877709.js index 1dac277a9055f..c2af27971a331 100644 --- a/js/src/jit-test/tests/ion/bug1877709.js +++ b/js/src/jit-test/tests/ion/bug1877709.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function testMathyFunction (f, inputs) { var results = []; for (var j = 0; j < inputs.length; ++j) diff --git a/js/src/jit-test/tests/ion/bug1921215.js b/js/src/jit-test/tests/ion/bug1921215.js index 52c6c938708b6..d3f6618f987f6 100644 --- a/js/src/jit-test/tests/ion/bug1921215.js +++ b/js/src/jit-test/tests/ion/bug1921215.js @@ -1,4 +1,4 @@ -// |jit-test| --fast-warmup; --no-threads; exitstatus: 6 +// |jit-test| --fast-warmup; --no-threads; exitstatus: 6; skip-if: getBuildConfiguration("wasi") timeout(0.05); function f() { var b = "".match(); diff --git a/js/src/jit-test/tests/ion/bug1987592.js b/js/src/jit-test/tests/ion/bug1987592.js index dcfeba31b1faa..36ef5509786ad 100644 --- a/js/src/jit-test/tests/ion/bug1987592.js +++ b/js/src/jit-test/tests/ion/bug1987592.js @@ -1,4 +1,4 @@ -// |jit-test| --ion-eager +// |jit-test| --ion-eager; skip-if: !this.Atomics const arr = new Int32Array(4096); let count = 0; diff --git a/js/src/jit-test/tests/ion/bug754720.js b/js/src/jit-test/tests/ion/bug754720.js index 9c8d813fe6eb3..dcdb3622774ce 100644 --- a/js/src/jit-test/tests/ion/bug754720.js +++ b/js/src/jit-test/tests/ion/bug754720.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function f2(a) { return f2.arguments; } diff --git a/js/src/jit-test/tests/ion/bug758991.js b/js/src/jit-test/tests/ion/bug758991.js index 093a75249aa41..83580fe7adfe3 100644 --- a/js/src/jit-test/tests/ion/bug758991.js +++ b/js/src/jit-test/tests/ion/bug758991.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Don't treat f.caller as a singleton property access, it // has a non-default getter. function f(obj) { diff --git a/js/src/jit-test/tests/ion/bug813784.js b/js/src/jit-test/tests/ion/bug813784.js index d3b0767135ff3..833ca27374059 100644 --- a/js/src/jit-test/tests/ion/bug813784.js +++ b/js/src/jit-test/tests/ion/bug813784.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() /* Test an inlined argument returns the arguments from the right function */ function get_arg_2() { return arguments[2]; } function test() { return get_arg_2(1,2,3); } diff --git a/js/src/jit-test/tests/ion/bug818023.js b/js/src/jit-test/tests/ion/bug818023.js index d572aaf243a16..916ac351e14ec 100644 --- a/js/src/jit-test/tests/ion/bug818023.js +++ b/js/src/jit-test/tests/ion/bug818023.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() Function.prototype.callX = Function.prototype.call; var x; function f() { diff --git a/js/src/jit-test/tests/ion/bug824473.js b/js/src/jit-test/tests/ion/bug824473.js index 93726b845fbd6..06660047dc2ae 100644 --- a/js/src/jit-test/tests/ion/bug824473.js +++ b/js/src/jit-test/tests/ion/bug824473.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function dumpArgs(i) { if (i == 90) return funapply.arguments.length; return [i]; } function funapply() { return dumpArgs.apply(undefined, arguments); } function test(i) { return funapply(i); } diff --git a/js/src/jit-test/tests/ion/bug835178.js b/js/src/jit-test/tests/ion/bug835178.js index ec23951924009..16199f630490a 100644 --- a/js/src/jit-test/tests/ion/bug835178.js +++ b/js/src/jit-test/tests/ion/bug835178.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function boo() { return foo.arguments[0] } function foo(a,b,c) { if (a == 0) {a = 2; return boo();} return a } function inlined() { return foo.apply({}, arguments); } diff --git a/js/src/jit-test/tests/ion/bug977966.js b/js/src/jit-test/tests/ion/bug977966.js index 6d80c5c376bb4..ade0c66738ca3 100644 --- a/js/src/jit-test/tests/ion/bug977966.js +++ b/js/src/jit-test/tests/ion/bug977966.js @@ -1,4 +1,4 @@ -// |jit-test| --ion-eager +// |jit-test| --ion-eager; skip-if: nightTierEnabled() function join_check() { var lengthWasCalled = false; diff --git a/js/src/jit-test/tests/ion/dce-with-rinstructions.js b/js/src/jit-test/tests/ion/dce-with-rinstructions.js index ace1d9f09d7df..d337c20ce035b 100644 --- a/js/src/jit-test/tests/ion/dce-with-rinstructions.js +++ b/js/src/jit-test/tests/ion/dce-with-rinstructions.js @@ -1,4 +1,4 @@ -// |jit-test| --ion-limit-script-size=off +// |jit-test| --ion-limit-script-size=off; skip-if: !this.Atomics setJitCompilerOption("baseline.warmup.trigger", 9); setJitCompilerOption("ion.warmup.trigger", 20); diff --git a/js/src/jit-test/tests/ion/is-constructing.js b/js/src/jit-test/tests/ion/is-constructing.js index 82224321e86c0..17c62a83db261 100644 --- a/js/src/jit-test/tests/ion/is-constructing.js +++ b/js/src/jit-test/tests/ion/is-constructing.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var isConstructing = getSelfHostedValue("IsConstructing"); function testBasic() { diff --git a/js/src/jit-test/tests/ion/recover-atomics-islockfree.js b/js/src/jit-test/tests/ion/recover-atomics-islockfree.js index 2a57afd49b31b..a0e2a089d130a 100644 --- a/js/src/jit-test/tests/ion/recover-atomics-islockfree.js +++ b/js/src/jit-test/tests/ion/recover-atomics-islockfree.js @@ -1,4 +1,4 @@ -// |jit-test| --fast-warmup; --ion-offthread-compile=off +// |jit-test| --fast-warmup; --ion-offthread-compile=off; skip-if: !this.Atomics function foo(n, trigger) { let result = Atomics.isLockFree(n * -1); diff --git a/js/src/jit-test/tests/ion/recover-int64tobigint.js b/js/src/jit-test/tests/ion/recover-int64tobigint.js index 84499b27d56a5..dbbc214424e60 100644 --- a/js/src/jit-test/tests/ion/recover-int64tobigint.js +++ b/js/src/jit-test/tests/ion/recover-int64tobigint.js @@ -1,4 +1,4 @@ -// |jit-test| --ion-limit-script-size=off +// |jit-test| --ion-limit-script-size=off; skip-if: !this.Atomics setJitCompilerOption("baseline.warmup.trigger", 9); setJitCompilerOption("ion.warmup.trigger", 20); diff --git a/js/src/jit-test/tests/ion/recover-lambdas-bug1133389.js b/js/src/jit-test/tests/ion/recover-lambdas-bug1133389.js index a1fc813b63a38..4759d52020ab4 100644 --- a/js/src/jit-test/tests/ion/recover-lambdas-bug1133389.js +++ b/js/src/jit-test/tests/ion/recover-lambdas-bug1133389.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() var o = {} Object.defineProperty(o, "p", { get: function() { diff --git a/js/src/jit-test/tests/ion/recover-lambdas.js b/js/src/jit-test/tests/ion/recover-lambdas.js index bec1dad254af8..daa6bb16b677d 100644 --- a/js/src/jit-test/tests/ion/recover-lambdas.js +++ b/js/src/jit-test/tests/ion/recover-lambdas.js @@ -1,4 +1,4 @@ -// |jit-test| --no-ion; --ion-osr=off +// |jit-test| --no-ion; --ion-osr=off; skip-if: nightTierEnabled() // Warp lacks Scalar Replacement support (bug 1650233). Re-evaluate after that // bug has been fixed. diff --git a/js/src/jit-test/tests/ion/recover-objects.js b/js/src/jit-test/tests/ion/recover-objects.js index 55588ba826edc..85b15e5bf8089 100644 --- a/js/src/jit-test/tests/ion/recover-objects.js +++ b/js/src/jit-test/tests/ion/recover-objects.js @@ -1,4 +1,4 @@ -// |jit-test| --ion-pruning=on; --fast-warmup; --baseline-offthread-compile=off +// |jit-test| --ion-pruning=on; --fast-warmup; --baseline-offthread-compile=off; skip-if: nightTierEnabled() var max = 200; diff --git a/js/src/jit-test/tests/ion/test-scalar-replacement-float32.js b/js/src/jit-test/tests/ion/test-scalar-replacement-float32.js index 3302381f14c2b..06e07d92c0ddf 100644 --- a/js/src/jit-test/tests/ion/test-scalar-replacement-float32.js +++ b/js/src/jit-test/tests/ion/test-scalar-replacement-float32.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() setJitCompilerOption("ion.warmup.trigger", 30); var max = 40; diff --git a/js/src/jit-test/tests/jaeger/argumentsOptimize-1.js b/js/src/jit-test/tests/jaeger/argumentsOptimize-1.js index 6ee2c6af14293..5254ac70d0f42 100644 --- a/js/src/jit-test/tests/jaeger/argumentsOptimize-1.js +++ b/js/src/jit-test/tests/jaeger/argumentsOptimize-1.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function bar() { foo.arguments.length = 10; diff --git a/js/src/jit-test/tests/jaeger/bug563000/eif-call-typechange.js b/js/src/jit-test/tests/jaeger/bug563000/eif-call-typechange.js index 486659a3360ce..bda0be7f4ebaa 100644 --- a/js/src/jit-test/tests/jaeger/bug563000/eif-call-typechange.js +++ b/js/src/jit-test/tests/jaeger/bug563000/eif-call-typechange.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); function callee() { diff --git a/js/src/jit-test/tests/jaeger/bug563000/eif-call.js b/js/src/jit-test/tests/jaeger/bug563000/eif-call.js index eff548c8b5dad..f2c3bcfac96d4 100644 --- a/js/src/jit-test/tests/jaeger/bug563000/eif-call.js +++ b/js/src/jit-test/tests/jaeger/bug563000/eif-call.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); function callee() { diff --git a/js/src/jit-test/tests/jaeger/bug563000/eif-getter-newvar.js b/js/src/jit-test/tests/jaeger/bug563000/eif-getter-newvar.js index 87e6923401a41..1567144bd05c3 100644 --- a/js/src/jit-test/tests/jaeger/bug563000/eif-getter-newvar.js +++ b/js/src/jit-test/tests/jaeger/bug563000/eif-getter-newvar.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); this.__defineGetter__("someProperty", function () { evalInFrame(1, "var x = 'success'"); }); diff --git a/js/src/jit-test/tests/jaeger/bug563000/eif-getter-typechange.js b/js/src/jit-test/tests/jaeger/bug563000/eif-getter-typechange.js index 067ec4dcdac0e..dbd78b72d768d 100644 --- a/js/src/jit-test/tests/jaeger/bug563000/eif-getter-typechange.js +++ b/js/src/jit-test/tests/jaeger/bug563000/eif-getter-typechange.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); this.__defineGetter__("someProperty", function () { evalInFrame(1, "var x = 'success'"); }); diff --git a/js/src/jit-test/tests/jaeger/bug563000/eif-getter.js b/js/src/jit-test/tests/jaeger/bug563000/eif-getter.js index e64492ce479c2..d7d5259f0cab5 100644 --- a/js/src/jit-test/tests/jaeger/bug563000/eif-getter.js +++ b/js/src/jit-test/tests/jaeger/bug563000/eif-getter.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); this.__defineGetter__("someProperty", function () { evalInFrame(1, "x = 'success'"); }); diff --git a/js/src/jit-test/tests/jaeger/bug563000/eif-global-newvar.js b/js/src/jit-test/tests/jaeger/bug563000/eif-global-newvar.js index eb5ed0adffb2b..d45ffe41602c5 100644 --- a/js/src/jit-test/tests/jaeger/bug563000/eif-global-newvar.js +++ b/js/src/jit-test/tests/jaeger/bug563000/eif-global-newvar.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); function callee() { diff --git a/js/src/jit-test/tests/jaeger/bug710780.js b/js/src/jit-test/tests/jaeger/bug710780.js index 84d2646921d1c..0e97a59a0f1e8 100644 --- a/js/src/jit-test/tests/jaeger/bug710780.js +++ b/js/src/jit-test/tests/jaeger/bug710780.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function foo() { function bar() { diff --git a/js/src/jit-test/tests/jaeger/getter-hook-2.js b/js/src/jit-test/tests/jaeger/getter-hook-2.js index 4361ce1262dc4..f78f4e0d740a6 100644 --- a/js/src/jit-test/tests/jaeger/getter-hook-2.js +++ b/js/src/jit-test/tests/jaeger/getter-hook-2.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // PIC on CALLPROP invoking getter hook. function foo(arr) { diff --git a/js/src/jit-test/tests/jaeger/invokeSessionGuard.js b/js/src/jit-test/tests/jaeger/invokeSessionGuard.js index 53bdbe5d6e011..79b86242e9d96 100644 --- a/js/src/jit-test/tests/jaeger/invokeSessionGuard.js +++ b/js/src/jit-test/tests/jaeger/invokeSessionGuard.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "evalInFrame.js"); [1,2,3,4,5,6,7,8].forEach( diff --git a/js/src/jit-test/tests/jaeger/loops/hoist-05.js b/js/src/jit-test/tests/jaeger/loops/hoist-05.js index b99b07893bee2..c1844abe0e184 100644 --- a/js/src/jit-test/tests/jaeger/loops/hoist-05.js +++ b/js/src/jit-test/tests/jaeger/loops/hoist-05.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function bar(x, i) { if (i == 50) foo.arguments[1] = 20; diff --git a/js/src/jit-test/tests/latin1/decompiler.js b/js/src/jit-test/tests/latin1/decompiler.js index 35f07847ed4bb..12877b142539b 100644 --- a/js/src/jit-test/tests/latin1/decompiler.js +++ b/js/src/jit-test/tests/latin1/decompiler.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Latin1 function f(someName) { someName(); diff --git a/js/src/jit-test/tests/modules/bug-1245518.js b/js/src/jit-test/tests/modules/bug-1245518.js index 857451cabdf85..80213caa083e9 100644 --- a/js/src/jit-test/tests/modules/bug-1245518.js +++ b/js/src/jit-test/tests/modules/bug-1245518.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() evalInFrame = function(global) { dbgGlobal = newGlobal({newCompartment: true}); dbg = new dbgGlobal.Debugger(); diff --git a/js/src/jit-test/tests/modules/bug-1498980.js b/js/src/jit-test/tests/modules/bug-1498980.js index f05dca6af568e..8e4590fa1a5e2 100644 --- a/js/src/jit-test/tests/modules/bug-1498980.js +++ b/js/src/jit-test/tests/modules/bug-1498980.js @@ -1,4 +1,4 @@ -// |jit-test| +// |jit-test|; skip-if: nightTierEnabled() dbgGlobal = newGlobal({newCompartment: true}); dbg = new dbgGlobal.Debugger; dbg.addDebuggee(this); diff --git a/js/src/jit-test/tests/modules/bug-1782496.js b/js/src/jit-test/tests/modules/bug-1782496.js index 33ea725a7be19..9dea3e5cfcf60 100644 --- a/js/src/jit-test/tests/modules/bug-1782496.js +++ b/js/src/jit-test/tests/modules/bug-1782496.js @@ -1,4 +1,4 @@ -// |jit-test| exitstatus: 6; allow-overrecursed +// |jit-test| exitstatus: 6; allow-overrecursed; skip-if: nightTierEnabled() setInterruptCallback(function() { import("javascript:null"); diff --git a/js/src/jit-test/tests/modules/failure-on-resume.js b/js/src/jit-test/tests/modules/failure-on-resume.js index d0e716b0bb8a4..4f1dfa728d69c 100644 --- a/js/src/jit-test/tests/modules/failure-on-resume.js +++ b/js/src/jit-test/tests/modules/failure-on-resume.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() const dbgGlobal = newGlobal({ newCompartment: true }); dbgGlobal.parent = this; dbgGlobal.eval(` diff --git a/js/src/jit-test/tests/night/define-elem-over-sparse.js b/js/src/jit-test/tests/night/define-elem-over-sparse.js new file mode 100644 index 0000000000000..46ab6158d504d --- /dev/null +++ b/js/src/jit-test/tests/night/define-elem-over-sparse.js @@ -0,0 +1,14 @@ +// DefineDataProperty over an array whose index is a sparse non-writable +// property must redefine it, not add a dense element beside it. +var a = [1]; +a.constructor = {}; +a.constructor[Symbol.species] = function(len) { + var q = new Array(0); + Object.defineProperty(q, 0, {value: 0, writable: false, configurable: true, enumerable: false}); + return q; +}; +var r = a.map(function() { return 2; }); +assertEq(r[0], 2); +assertEq(Object.getOwnPropertyNames(r).join(","), "0,length"); +assertEq(delete r[0], true); +assertEq(r.hasOwnProperty(0), false); diff --git a/js/src/jit-test/tests/night/exception-unwinds-lexical-env.js b/js/src/jit-test/tests/night/exception-unwinds-lexical-env.js new file mode 100644 index 0000000000000..4969acb98b85e --- /dev/null +++ b/js/src/jit-test/tests/night/exception-unwinds-lexical-env.js @@ -0,0 +1,48 @@ +// A throw from inside a block with its own environment must leave the +// handler at the Try's environment: aliased reads there see the outer scope. +function f() { + let t = "outer"; + function g() { return t; } + try { + { let u = "inner"; function h() { return u; } throw 1; } + } catch (e) { + return t; + } +} +assertEq(f(), "outer"); + +function f2() { + let t = "outer"; + function g() { return t; } + var seen; + try { + { let u = "inner"; function h() { return u; } throw 1; } + } finally { + seen = t; + } +} +try { f2(); } catch (e) {} + +function f3() { + var t = "outer"; + function g() { return t; } + try { + { let u = "inner"; function h() { return u; } + { let w = "inner2"; function h2() { return w; } throw 1; } } + } catch (e) { + t = t + "+caught"; + return g(); + } +} +assertEq(f3(), "outer+caught"); + +// The async form: the generator object is an aliased variable, and the +// rejection path read it after unwinding. +var rejected = null; +(async function () { + const t = { then(res, rej) { rej("R"); } }; + function* gen() { yield t; } + await Array.fromAsync(gen()); +})().then(() => { rejected = "resolved"; }, e => { rejected = e; }); +drainJobQueue(); +assertEq(rejected, "R"); diff --git a/js/src/jit-test/tests/night/function-name-lazy-resolve.js b/js/src/jit-test/tests/night/function-name-lazy-resolve.js new file mode 100644 index 0000000000000..328f5b73c7f9b --- /dev/null +++ b/js/src/jit-test/tests/night/function-name-lazy-resolve.js @@ -0,0 +1,8 @@ +// A function's `name` is resolved lazily. A property cache populated by a +// function whose own `name` was deleted (same shape as an unresolved one) +// must not serve Function.prototype.name to a fresh function. +function sloppy(f) { var name = f.name; delete f.name; return [name, f.name]; } +function strictRead(f) { "use strict"; return f.name; } +assertEq(sloppy(function f() {}).join(","), "f,"); +assertEq(strictRead(function f() {}), "f"); +assertEq(strictRead(function g() {}), "g"); diff --git a/js/src/jit-test/tests/night/gc-callback-keeps-cache-purge.js b/js/src/jit-test/tests/night/gc-callback-keeps-cache-purge.js new file mode 100644 index 0000000000000..8ae3a89176121 --- /dev/null +++ b/js/src/jit-test/tests/night/gc-callback-keeps-cache-purge.js @@ -0,0 +1,11 @@ +// The shell's setGCCallback replaces the engine's GC callback; the tier's +// inline caches must still be purged around a major GC. +function garbage() { var x; for (var i = 0; i < 100000; i++) x = { i: i }; } +setGCCallback({ action: "majorGC", depth: 1, phases: "both" }); +garbage(); +gc(); +garbage(); +setGCCallback({ action: "minorGC", phases: "begin" }); +garbage(); +gc(); +garbage(); diff --git a/js/src/jit-test/tests/night/regexp-exec-lastindex-read.js b/js/src/jit-test/tests/night/regexp-exec-lastindex-read.js new file mode 100644 index 0000000000000..926798b30e554 --- /dev/null +++ b/js/src/jit-test/tests/night/regexp-exec-lastindex-read.js @@ -0,0 +1,14 @@ +// RegExpBuiltinExec reads lastIndex for every regexp, so a valueOf on it is +// observable even when the regexp is neither global nor sticky. +var gets = 0; +var counter = { valueOf: function() { gets++; return 0; } }; +var r = /a/; +r.lastIndex = counter; +assertEq(r.exec("nbc"), null); +assertEq(r.lastIndex, counter); +assertEq(gets, 1); +var called = 0; +var re = /./; +re.lastIndex = { toString: function() { called++; return "0"; } }; +re.exec("."); +assertEq(called, 1); diff --git a/js/src/jit-test/tests/profiler/bug2002982.js b/js/src/jit-test/tests/profiler/bug2002982.js index e6ced6f97d803..43a6a4d313aca 100644 --- a/js/src/jit-test/tests/profiler/bug2002982.js +++ b/js/src/jit-test/tests/profiler/bug2002982.js @@ -1,3 +1,5 @@ +// |jit-test| skip-if: nightTierEnabled() +// (AOT-executed scripts never baseline-compile, so no profiler script sources register.) // Test that script sources are properly registered on consecutive profiler runs. // Bug 2002982: When the profiler is disabled and re-enabled, script sources // need to be re-registered because the profiler's ScriptSources hashset is diff --git a/js/src/jit-test/tests/profiler/interpreter-stacks.js b/js/src/jit-test/tests/profiler/interpreter-stacks.js index 737a8fce181ed..392c74c754fee 100644 --- a/js/src/jit-test/tests/profiler/interpreter-stacks.js +++ b/js/src/jit-test/tests/profiler/interpreter-stacks.js @@ -1,4 +1,4 @@ -// |jit-test| --no-blinterp +// |jit-test| --no-blinterp; skip-if: nightTierEnabled() // Disable Baseline Interpreter and JITs because we only read information about // C++ Interpreter profiling frames. diff --git a/js/src/jit-test/tests/promise/newpromisecapability-error-message.js b/js/src/jit-test/tests/promise/newpromisecapability-error-message.js index 4fb96013f4f15..b1d891b51ca28 100644 --- a/js/src/jit-test/tests/promise/newpromisecapability-error-message.js +++ b/js/src/jit-test/tests/promise/newpromisecapability-error-message.js @@ -1,4 +1,4 @@ -// |jit-test| skip-if: getBuildConfiguration('pbl') +// |jit-test| skip-if: getBuildConfiguration('pbl'); skip-if: nightTierEnabled() // (justification: PBL does not invoke the decompiler in the same way and so // will not have an error message referring to the specific value name) load(libdir + "asserts.js"); diff --git a/js/src/jit-test/tests/realms/bug1518821.js b/js/src/jit-test/tests/realms/bug1518821.js index 03a217be81403..de62c6326ff34 100644 --- a/js/src/jit-test/tests/realms/bug1518821.js +++ b/js/src/jit-test/tests/realms/bug1518821.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "asserts.js"); var g = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/realms/scripted-caller-global.js b/js/src/jit-test/tests/realms/scripted-caller-global.js index 15290e90b59f9..49a47f001328b 100644 --- a/js/src/jit-test/tests/realms/scripted-caller-global.js +++ b/js/src/jit-test/tests/realms/scripted-caller-global.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() assertEq(scriptedCallerGlobal(), this); var g = newGlobal(); diff --git a/js/src/jit-test/tests/resist-fingerprinting/math-fdlibm-sincostan.js b/js/src/jit-test/tests/resist-fingerprinting/math-fdlibm-sincostan.js index 0fe5445e79237..958992b3df3b7 100644 --- a/js/src/jit-test/tests/resist-fingerprinting/math-fdlibm-sincostan.js +++ b/js/src/jit-test/tests/resist-fingerprinting/math-fdlibm-sincostan.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: getBuildConfiguration("wasi") let g = newGlobal({alwaysUseFdlibm: true}); // Adapted from https://github.com/arkenfox/TZP/blob/master/tests/math.html diff --git a/js/src/jit-test/tests/saved-stacks/1438121-async-function.js b/js/src/jit-test/tests/saved-stacks/1438121-async-function.js index 87fc9bab0ada3..239198cb8a2c6 100644 --- a/js/src/jit-test/tests/saved-stacks/1438121-async-function.js +++ b/js/src/jit-test/tests/saved-stacks/1438121-async-function.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() const mainGlobal = this; const debuggerGlobal = newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/saved-stacks/asm-frames.js b/js/src/jit-test/tests/saved-stacks/asm-frames.js index 3667122532566..c362cce05d2db 100644 --- a/js/src/jit-test/tests/saved-stacks/asm-frames.js +++ b/js/src/jit-test/tests/saved-stacks/asm-frames.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function AsmModule(stdlib, foreign, heap) { "use asm"; var ffi = foreign.t; @@ -27,11 +28,11 @@ print(stack); assertEq(stack.functionDisplayName, "tester"); assertEq(stack.parent.functionDisplayName, "doTest"); -assertEq(stack.parent.line, 6); +assertEq(stack.parent.line, 7); assertEq(stack.parent.parent.functionDisplayName, "test"); -assertEq(stack.parent.parent.line, 10); +assertEq(stack.parent.parent.line, 11); -assertEq(stack.parent.parent.parent.line, 24); +assertEq(stack.parent.parent.parent.line, 25); assertEq(stack.parent.parent.parent.parent, null); diff --git a/js/src/jit-test/tests/saved-stacks/async-implicit.js b/js/src/jit-test/tests/saved-stacks/async-implicit.js index 26b377aca039a..b9b2f5c2155ec 100644 --- a/js/src/jit-test/tests/saved-stacks/async-implicit.js +++ b/js/src/jit-test/tests/saved-stacks/async-implicit.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test AutoSetAsyncStackForNewCalls's IMPLICIT kind. // Given a SavedFrame stack, return a string listing the frame's function names diff --git a/js/src/jit-test/tests/saved-stacks/async-livecache.js b/js/src/jit-test/tests/saved-stacks/async-livecache.js index 1034b5fc49921..c10139111d637 100644 --- a/js/src/jit-test/tests/saved-stacks/async-livecache.js +++ b/js/src/jit-test/tests/saved-stacks/async-livecache.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Async stacks should not supplant LiveSavedFrameCache hits. top(); diff --git a/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js b/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js index bada5b1ac9f19..404a267682237 100644 --- a/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js +++ b/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that async stacks are limited on recursion. const defaultAsyncStackLimit = 60; diff --git a/js/src/jit-test/tests/saved-stacks/async.js b/js/src/jit-test/tests/saved-stacks/async.js index 6ab4546a7c7af..68e3c03c3c7a3 100644 --- a/js/src/jit-test/tests/saved-stacks/async.js +++ b/js/src/jit-test/tests/saved-stacks/async.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test calling a function using a previously captured stack as an async stack. function getAsyncStack() { diff --git a/js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js b/js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js index 64029d8cb0a72..889308041593b 100644 --- a/js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js +++ b/js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js @@ -1,4 +1,4 @@ -// |jit-test| --ion-eager; --no-threads; +// |jit-test| --ion-eager; --no-threads; skip-if: nightTierEnabled() // This test ensures that debugger eval on an ion frame is able to correctly // follow the debugger eval frame link to its parent frame. diff --git a/js/src/jit-test/tests/saved-stacks/bug-1509420.js b/js/src/jit-test/tests/saved-stacks/bug-1509420.js index 87cd0b7f21150..50323f15ef45c 100644 --- a/js/src/jit-test/tests/saved-stacks/bug-1509420.js +++ b/js/src/jit-test/tests/saved-stacks/bug-1509420.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // bindtoAsyncStack shouldn't choke on CCWs of functions. var g = newGlobal(); diff --git a/js/src/jit-test/tests/saved-stacks/bug-1744495.js b/js/src/jit-test/tests/saved-stacks/bug-1744495.js index 4ce56f423530a..2e95f2d7504ac 100644 --- a/js/src/jit-test/tests/saved-stacks/bug-1744495.js +++ b/js/src/jit-test/tests/saved-stacks/bug-1744495.js @@ -1,4 +1,4 @@ -// |jit-test| --fast-warmup; --more-compartments +// |jit-test| --fast-warmup; --more-compartments; skip-if: nightTierEnabled() enableTrackAllocations() e = function(a) { diff --git a/js/src/jit-test/tests/saved-stacks/bug1907801.js b/js/src/jit-test/tests/saved-stacks/bug1907801.js index d927a41a9dbea..daf05fbb7dbf1 100644 --- a/js/src/jit-test/tests/saved-stacks/bug1907801.js +++ b/js/src/jit-test/tests/saved-stacks/bug1907801.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() let g = newGlobal({ newCompartment: true }); let dbg = Debugger(g); diff --git a/js/src/jit-test/tests/saved-stacks/evals.js b/js/src/jit-test/tests/saved-stacks/evals.js index 41a0f9111c6ab..bf045c0967085 100644 --- a/js/src/jit-test/tests/saved-stacks/evals.js +++ b/js/src/jit-test/tests/saved-stacks/evals.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can save stacks with direct and indirect eval calls. const directEval = (function iife() { diff --git a/js/src/jit-test/tests/saved-stacks/function-display-name.js b/js/src/jit-test/tests/saved-stacks/function-display-name.js index f10b7de6bd6d0..675eafd70d040 100644 --- a/js/src/jit-test/tests/saved-stacks/function-display-name.js +++ b/js/src/jit-test/tests/saved-stacks/function-display-name.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test the functionDisplayName of SavedFrame instances. function uno() { return dos(); } diff --git a/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js b/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js index cf2646f471fef..62522caa0d5b6 100644 --- a/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js +++ b/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that SavedFrame instances get removed from the SavedStacks frames cache // after a GC. diff --git a/js/src/jit-test/tests/saved-stacks/generators.js b/js/src/jit-test/tests/saved-stacks/generators.js index 2878997580a9d..1fe2dabffbc37 100644 --- a/js/src/jit-test/tests/saved-stacks/generators.js +++ b/js/src/jit-test/tests/saved-stacks/generators.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can save stacks which have generator frames. const { value: frame } = (function iife1() { diff --git a/js/src/jit-test/tests/saved-stacks/get-set.js b/js/src/jit-test/tests/saved-stacks/get-set.js index be2e20739937a..577673cb7ce85 100644 --- a/js/src/jit-test/tests/saved-stacks/get-set.js +++ b/js/src/jit-test/tests/saved-stacks/get-set.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can save stacks with getter and setter function frames. function assertStackLengthEq(stack, expectedLength) { diff --git a/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js b/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js index 9a892f20bf001..d87999f318671 100644 --- a/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js +++ b/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that you can't call the SavedFrame constructor and can only use // SavedFrame's getters on SavedFrame instances. diff --git a/js/src/jit-test/tests/saved-stacks/max-frame-count.js b/js/src/jit-test/tests/saved-stacks/max-frame-count.js index 17c37517651cf..19feaca9c8057 100644 --- a/js/src/jit-test/tests/saved-stacks/max-frame-count.js +++ b/js/src/jit-test/tests/saved-stacks/max-frame-count.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can capture only the N newest frames. // This is the maxFrameCount argument to JS::CaptureCurrentStack. diff --git a/js/src/jit-test/tests/saved-stacks/native-calls.js b/js/src/jit-test/tests/saved-stacks/native-calls.js index 4b12ad7383aa0..a0bf5bedc9711 100644 --- a/js/src/jit-test/tests/saved-stacks/native-calls.js +++ b/js/src/jit-test/tests/saved-stacks/native-calls.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can save stacks with native code on the stack. // Unlike Array.prototype.map, Array.prototype.filter is not self-hosted. diff --git a/js/src/jit-test/tests/saved-stacks/proxy-handlers.js b/js/src/jit-test/tests/saved-stacks/proxy-handlers.js index 7ad1f6dc68215..aeec642060f22 100644 --- a/js/src/jit-test/tests/saved-stacks/proxy-handlers.js +++ b/js/src/jit-test/tests/saved-stacks/proxy-handlers.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can save stacks with proxy handler frames. const stack = (function iife() { diff --git a/js/src/jit-test/tests/saved-stacks/self-hosted.js b/js/src/jit-test/tests/saved-stacks/self-hosted.js index 88f8ce20070c2..2c58817e86a3d 100644 --- a/js/src/jit-test/tests/saved-stacks/self-hosted.js +++ b/js/src/jit-test/tests/saved-stacks/self-hosted.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that we can save stacks with self-hosted function frames in them. const map = (function () { diff --git a/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js b/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js index c6b4332dd9776..d433a8318a72f 100644 --- a/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js +++ b/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that parent frames are shared when the older portions of two stacks are // the same. diff --git a/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js b/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js index 2f867d8f3da7f..6404a974abdf6 100644 --- a/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js +++ b/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // Test that stringify'ing a saved frame with self-hosted parent frames doesn't // include the self-hosted parent frame in the output. diff --git a/js/src/jit-test/tests/self-hosting/method-called-on-incompatible.js b/js/src/jit-test/tests/self-hosting/method-called-on-incompatible.js index 2a48991f66a29..024d2df79e0c2 100644 --- a/js/src/jit-test/tests/self-hosting/method-called-on-incompatible.js +++ b/js/src/jit-test/tests/self-hosting/method-called-on-incompatible.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() load(libdir + "asserts.js"); assertTypeErrorMessage(() => Set.prototype.forEach.call({}), "forEach method called on incompatible Object"); diff --git a/js/src/jit-test/tests/self-hosting/relazify.js b/js/src/jit-test/tests/self-hosting/relazify.js index b335f8026c4d7..05744f4c510ce 100644 --- a/js/src/jit-test/tests/self-hosting/relazify.js +++ b/js/src/jit-test/tests/self-hosting/relazify.js @@ -1,4 +1,5 @@ -// |jit-test| skip-if: isLcovEnabled() +// |jit-test| skip-if: isLcovEnabled(); skip-if: nightTierEnabled() +// (AOT tier eagerly delazifies and compiles self-hosted builtins.) // Self-hosted builtins use a special form of lazy function, but still can be // delazified in some cases. diff --git a/js/src/jit-test/tests/self-test/baselineCompile-Bug1444894.js b/js/src/jit-test/tests/self-test/baselineCompile-Bug1444894.js index 768d4b655c474..0aa3f086dfef5 100644 --- a/js/src/jit-test/tests/self-test/baselineCompile-Bug1444894.js +++ b/js/src/jit-test/tests/self-test/baselineCompile-Bug1444894.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() if (typeof baselineCompile == "function") { gc(); diff --git a/js/src/jit-test/tests/sharedbuf/growable-sab-over-mailbox.js b/js/src/jit-test/tests/sharedbuf/growable-sab-over-mailbox.js index a5d8ee25138a9..e6dd17822a3bc 100644 --- a/js/src/jit-test/tests/sharedbuf/growable-sab-over-mailbox.js +++ b/js/src/jit-test/tests/sharedbuf/growable-sab-over-mailbox.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer var gsab = new SharedArrayBuffer(4, {maxByteLength: 16}); // Test byte lengths are correct. diff --git a/js/src/jit-test/tests/structured-clone/growable-shared-array-buffers.js b/js/src/jit-test/tests/structured-clone/growable-shared-array-buffers.js index fe67273e8ab3a..9b58187bbf11c 100644 --- a/js/src/jit-test/tests/structured-clone/growable-shared-array-buffers.js +++ b/js/src/jit-test/tests/structured-clone/growable-shared-array-buffers.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer const scopes = [ "SameProcess", ]; diff --git a/js/src/jit-test/tests/structured-clone/saved-stack.js b/js/src/jit-test/tests/structured-clone/saved-stack.js index dd2d4a3240146..19b51d89a5d82 100644 --- a/js/src/jit-test/tests/structured-clone/saved-stack.js +++ b/js/src/jit-test/tests/structured-clone/saved-stack.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() // The following binary data was created with: // JS_STRUCTURED_CLONE_VERSION = 8 // diff --git a/js/src/jit-test/tests/structured-clone/tenuring.js b/js/src/jit-test/tests/structured-clone/tenuring.js index cec53a6956a25..6e109234c228e 100644 --- a/js/src/jit-test/tests/structured-clone/tenuring.js +++ b/js/src/jit-test/tests/structured-clone/tenuring.js @@ -1,3 +1,5 @@ +// |jit-test| skip-if: nightTierEnabled() +// (AOT literal evaluation allocates nursery strings; the minor-GC count differs.) // Check that we switch to allocating in the tenured heap after the first // nursery collection. diff --git a/js/src/jit-test/tests/typedarray/arraybuffer-pin.js b/js/src/jit-test/tests/typedarray/arraybuffer-pin.js index 03d4fd8109909..91ea495110040 100644 --- a/js/src/jit-test/tests/typedarray/arraybuffer-pin.js +++ b/js/src/jit-test/tests/typedarray/arraybuffer-pin.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer load(libdir + "asserts.js"); var ab_inline = new ArrayBuffer(4); diff --git a/js/src/jit-test/tests/typedarray/arraybuffer-transfer.js b/js/src/jit-test/tests/typedarray/arraybuffer-transfer.js index eacde9c7df8be..032396308048c 100644 --- a/js/src/jit-test/tests/typedarray/arraybuffer-transfer.js +++ b/js/src/jit-test/tests/typedarray/arraybuffer-transfer.js @@ -1,4 +1,4 @@ -// |jit-test| skip-variant-if: --ion-eager, getBuildConfiguration("simulator") +// |jit-test| skip-variant-if: --ion-eager, getBuildConfiguration("simulator"); skip-if: getBuildConfiguration("wasi") // Slow in simulators with --ion-eager. diff --git a/js/src/jit-test/tests/typedarray/construct-with-growable-sharedarraybuffer.js b/js/src/jit-test/tests/typedarray/construct-with-growable-sharedarraybuffer.js index f593fb5858304..70c75673ee18f 100644 --- a/js/src/jit-test/tests/typedarray/construct-with-growable-sharedarraybuffer.js +++ b/js/src/jit-test/tests/typedarray/construct-with-growable-sharedarraybuffer.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer // Test TypedArray constructor when called with growable SharedArrayBuffers. function testSharedArrayBuffer() { diff --git a/js/src/jit-test/tests/typedarray/growable-sharedarraybuffer-bytelength.js b/js/src/jit-test/tests/typedarray/growable-sharedarraybuffer-bytelength.js index 09c6b81fdc551..d804dc6ff90c7 100644 --- a/js/src/jit-test/tests/typedarray/growable-sharedarraybuffer-bytelength.js +++ b/js/src/jit-test/tests/typedarray/growable-sharedarraybuffer-bytelength.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function testGrowableSharedArrayBuffer() { for (let i = 0; i < 4; ++i) { let sab = new SharedArrayBuffer(i, {maxByteLength: i + 100}); diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-bytelength-with-sab.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-bytelength-with-sab.js index 0e1ebc2f4987a..632beb7f4cf38 100644 --- a/js/src/jit-test/tests/typedarray/resizable-typedarray-bytelength-with-sab.js +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-bytelength-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function testResizableArrayBuffer() { for (let i = 0; i < 4; ++i) { let sab = new SharedArrayBuffer(i, {maxByteLength: i + 100}); diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-byteoffset-sab.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-byteoffset-sab.js index 62cfcaa6960a5..e10a8c2ce0f88 100644 --- a/js/src/jit-test/tests/typedarray/resizable-typedarray-byteoffset-sab.js +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-byteoffset-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function testResizableArrayBufferAutoLength() { for (let i = 0; i < 4; ++i) { let sab = new SharedArrayBuffer(i, {maxByteLength: i + 100}); diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-get-elem-with-sab.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-get-elem-with-sab.js index 153166e56a736..6c4850e2afa23 100644 --- a/js/src/jit-test/tests/typedarray/resizable-typedarray-get-elem-with-sab.js +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-get-elem-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer const TypedArrays = [ Int8Array, Uint8Array, diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-has-elem-with-sab.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-has-elem-with-sab.js index c7fc5a8b8547a..a09b2fecad573 100644 --- a/js/src/jit-test/tests/typedarray/resizable-typedarray-has-elem-with-sab.js +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-has-elem-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer const TypedArrays = [ Int8Array, Uint8Array, diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-intrinsic-typedArrayLength.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-intrinsic-typedArrayLength.js index d6fd6de61b687..3a3e8b920b7a3 100644 --- a/js/src/jit-test/tests/typedarray/resizable-typedarray-intrinsic-typedArrayLength.js +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-intrinsic-typedArrayLength.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer load(libdir + "asserts.js"); const TypedArrayLength = getSelfHostedValue("TypedArrayLength"); diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-length-with-sab.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-length-with-sab.js index 8e2360e423c2d..6214896140708 100644 --- a/js/src/jit-test/tests/typedarray/resizable-typedarray-length-with-sab.js +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-length-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer function testResizableArrayBuffer() { for (let i = 0; i < 4; ++i) { let sab = new SharedArrayBuffer(i, {maxByteLength: i + 100}); diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-set-elem-with-sab.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-set-elem-with-sab.js index 2b92b90843daf..9e193d61d2cc6 100644 --- a/js/src/jit-test/tests/typedarray/resizable-typedarray-set-elem-with-sab.js +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-set-elem-with-sab.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: !this.SharedArrayBuffer const TypedArrays = [ Int8Array, Uint8Array, diff --git a/js/src/jit-test/tests/warp/throw-exception-stack-location.js b/js/src/jit-test/tests/warp/throw-exception-stack-location.js index bcb7329b05747..41d818fc53de3 100644 --- a/js/src/jit-test/tests/warp/throw-exception-stack-location.js +++ b/js/src/jit-test/tests/warp/throw-exception-stack-location.js @@ -1,3 +1,4 @@ +// |jit-test| skip-if: nightTierEnabled() function throwValue(value) { throw value; } diff --git a/js/src/moz.build b/js/src/moz.build index ae5af29eda3fc..19fb2ba1a24f3 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -47,9 +47,16 @@ for stlfile in ["jsdate.*", "jsnum.*"]: with Files("builtin/intl/*"): BUG_COMPONENT = component_intl +if CONFIG["ENABLE_JS_NIGHTMONKEY"]: + DIRS += [ + "night", + ] + if not CONFIG["JS_DISABLE_SHELL"]: DIRS += [ "rust", + ] + DIRS += [ "shell", ] @@ -168,6 +175,7 @@ EXPORTS.js += [ "../public/MemoryMetrics.h", "../public/Modules.h", "../public/NativeStackLimits.h", + "../public/NightMonkey.h", "../public/Object.h", "../public/ObjectWithStashedPointer.h", "../public/Prefs.h", @@ -705,6 +713,4 @@ if CONFIG["USE_LIBZ_RS"]: DEFINES["USE_LIBZ_RS"] = True if CONFIG["ENABLE_JS_PBL_WEVAL"]: - LOCAL_INCLUDES += [ - "../../third_party/weval" - ] + LOCAL_INCLUDES += ["../../third_party/weval"] diff --git a/js/src/night/README.md b/js/src/night/README.md new file mode 100644 index 0000000000000..9f3ec94502c0d --- /dev/null +++ b/js/src/night/README.md @@ -0,0 +1,254 @@ +# NightMonkey: AOT JavaScript-to-WebAssembly compilation + +**NightMonkey** is an ahead-of-time JS-to-Wasm compilation tier built +inside SpiderMonkey. NIGHT expands to *Nonlocal Inference with Guiding +Heuristics for Types*: an optimistic whole-program type analysis guides +code generation, with dynamic guards for correctness. (The night monkey, +genus *Aotus*, is the only truly nocturnal monkey: it does its work in +the night, before the program runs during the day.) + +Each JS function's bytecode is compiled to a WebAssembly function that +runs alongside the runtime compiled to Wasm. There are two modes of use: + +- **Snapshot** (the shipping flow): the `nightmonkey` host binary drives + Wizer in-process to snapshot the runtime plus loaded user program (or + processes an existing Wizer snapshot), reads out JS bytecode and heap + objects (such as prototype objects), and rewrites that snapshot with + compiled bodies. +- **In-process** (the testing flow): the JS shell compiled to Wasm runs + under `wasm-jit-runner`, walks its own live heap, compiles the script + tree, and injects the bodies into its running instance via runner + hostcalls (`--night-inprocess`). A drop-in shell for jit-tests. + +NightMonkey has a two-part structure: an *optimistic static type +analysis* and a *guard-based codegen backend*. The idea is that we: + +1. "Predict" types statically, using a model of JavaScript semantics + that is intentionally optimistic (elides corner-cases). We call this + the "likelier-types analysis" (in a nod to the initial version of the + analysis, the "likely-types analysis"; this one is a little better). + +2. Generate an optimistic Wasm body for a given JS function bytecode + body, using those predicted types. + +3. Insert dynamic guards checking those assumptions, with fallbacks to a + fully generic (but still compiled!) Wasm body. + +The *key constraint* that NightMonkey adheres to, and attempts to solve: +we cannot derive type information, or any other profiling information, +by observing a running program. In other words, unlike the standard JIT +approach based on the "JIT hypothesis" (that a warmed-up program will +reach a steady state with stable types, which we can then specialize +for), we must decide any specialization we will do ahead-of-time, based +on whatever analysis or heuristics we can come up with. The thing we +permit ourselves in return is much more analysis time: unlike a JIT +engine, we do not need to compile in milliseconds. + +NightMonkey performs its analysis using a whole-program, call-sensitive, +points-to (heap abstraction) + callgraph analysis, over a lattice that +is a hybrid of a Steensgaard (union-find-based) and capped Andersen +(points-to-set/membership-based) design. + +The codegen using the types that come out of this analysis is then a +"two-track" approach: there is one optimistic track that adheres to +"type contexts" that are maximally optimal, and one fully generic track. +(Earlier experiments tried to do more multiversioning, a la Static Basic +Block Versioning, but that did not converge well.) + +## Layout + +| Path | Contents | +|---|---| +| `compiler/` | The compiler crate (`night-compiler`). | +| `compiler/night-compiler.h` | C ABI between SpiderMonkey and the compiler. | +| `compiler/src/source.rs`, `src/source/ffi.rs` | The `Source` object graph: the sole input to the compiler. | +| `compiler/src/bytecode.rs` | Bytecode parser and `OpcodeVisitor` (generated `JSOp` enum from Opcodes.h). | +| `compiler/src/options.rs` | `Options`/`Diagnostics`: the entire configuration surface. | +| `compiler/src/likelier/` | The speculative likely-types analysis (`scan`/`heap`/`calls`/`engine`/`emit`/`dump`). | +| `compiler/src/opsem.rs` | Interval algebra and op semantics; the vocabulary shared by analysis and codegen. | +| `compiler/src/facts.rs` | `LikelyFacts`: the analysis-to-codegen fact contract. | +| `compiler/src/wasm/bbv.rs` | The workqueue-BBV bytecode-to-Wasm codegen driver. | +| `compiler/src/wasm/translate.rs` | Shared translation substrate: `Helpers`/`AtomTable`/`Outcome`/ctx types, layout constants. | +| `compiler/src/wasm/regex.rs` | The regex AOT compiler (irregexp bytecode to Wasm matchers). | +| `compiler/src/wasm/mod.rs` | The `layout_env` / `translate_all` seams: analysis prepass, reserved linear-memory region layout, body translation, table patching. | +| `compiler/src/wasm/inprocess.rs` | In-process batch builder for the runner hostcalls. | +| `runtime/` | The night runtime: `NightRuntime.cpp` is the `night_runtime_*` C ABI generated code calls, in front of the engine halves it forwards to -- `NightOps.cpp` (bytecode ops), `NightInlineCaches.cpp` (property-cache populate and replay), `NightInlineHeap.cpp` (inline allocation, write barriers, and the baked-layout asserts), `NightGenerator.cpp`, `NightRegExp.cpp`. `NightEntry.cpp` is the other direction: entering compiled bodies. Plus the value stack and snapshot registration/activation/capture. Linked into the shell under `--enable-nightmonkey`. | +| `snapshot/` | Snapshot/live-heap reader crate (`night-snapshot`): parses the registration block and walks the script graph into a `Source`. | +| `nightmonkey/` | The `nightmonkey` binary: snapshot in, AOT-compiled module out. The optional `wizen` Cargo feature also accepts programs and drives wizer as a library. | +| `wasm-jit-runner/` | Wasmtime-based runner exposing function-injection hostcalls for the in-process flow. | +| `configs/` | The mozconfigs (see "Builds"). | +| `docs/` | `DESIGN.md`, `INTEGRATION.md`, `TODO`. | +| `tools/` | Profiling, benchmarking, and visualization helpers (`viz.py`, `opprof.py`, `pairab.sh`, ...). | +| `inproc-shell.sh` | `jit_test.py` shim running the wasm shell under the runner. | + +## Build flags + +- `--enable-nightmonkey` (wasm32 targets only; configure errors otherwise): + links the night runtime into the shell, enables wizer snapshot + registration, and builds the `nightmonkey` host binary into + `dist/host/bin`. +- `--enable-nightmonkey-inprocess` (requires `--enable-nightmonkey`): links + the compiler crate into the shell (the `--night-inprocess` flag), adds the + wasm-jit-runner hostcall imports to the shell module, and builds the + `wasm-jit-runner` host binary into `dist/bin`. + +## Prerequisites + +- A Rust toolchain with the `wasm32-wasip1` target + (`rustup target add wasm32-wasip1`). +- `wasmtime` on `$PATH` or at `$HOME/bin/wasmtime`, to run compiled modules. + +Wizer is a library dependency of `nightmonkey`; there is nothing to install. + +All `./mach build` invocations run from the repo root, against the whole +tree (never a subdirectory), and never concurrently with another build. + +## Flow 1: snapshot (the shipping flow) + +Step 1 — build the wizerable wasm shell (also produces the compiler): + +``` +MOZCONFIG=js/src/night/configs/mozconfig-nightmonkey ./mach build +# -> obj-nightmonkey/dist/bin/js (wasm32-wasi shell) +# -> obj-nightmonkey/dist/host/bin/nightmonkey (the AOT compiler) +``` + +Step 2 — compile a program, in one command: + +``` +obj-nightmonkey/dist/host/bin/nightmonkey \ + --shell obj-nightmonkey/dist/bin/js program.js -o program-aot.wasm +wasmtime run program-aot.wasm +``` + +`nightmonkey` snapshots the shell with wizer in-process, then rewrites the +snapshot with compiled bodies. The program's top level runs *during* +wizening, so setup and class construction are captured in the image, and the +resumed snapshot calls the program's global `main()`. + +Passing a pre-made snapshot instead of a `.js` file also works, and is the +fast inner loop for compiler work: + +``` +nightmonkey --shell program.js --keep-snapshot snap.wasm -o out.wasm +nightmonkey snap.wasm -o out.wasm # recompile without re-wizening +``` + +`nightmonkey --help` lists the diagnostics (`--stats`, `--dump-bytecode`, +`--dump-bbv`, `--dump-facts`, `--dump-graph`, `--viz`, `--viz-lower`, +`--viz-facts`) and the compilation options (`--force-interp`, +`--keep-names`). `--dump-bytecode` +takes an optional comma-separated source-id list +(`--dump-bytecode=145,153`); a whole-bundle disassembly is megabytes. +Debug sections are stripped by default; `--keep-names` retains them. + +## Flow 2: in-process (drop-in shell for jit-tests) + +Step 1 — build the in-process shell and the runner: + +``` +MOZCONFIG=js/src/night/configs/mozconfig-nightmonkey-inprocess ./mach build +# -> obj-nightmonkey-inprocess/dist/bin/js (wasm32-wasi shell) +# -> obj-nightmonkey-inprocess/dist/bin/wasm-jit-runner (the test host) +# -> obj-nightmonkey-inprocess/dist/host/bin/nightmonkey (also usable for flow 1) +``` + +Step 2 — run a program: + +``` +obj-nightmonkey-inprocess/dist/bin/wasm-jit-runner \ + --dir / --cache-dir ~/.cache/wjr \ + obj-nightmonkey-inprocess/dist/bin/js -- --night-inprocess /abs/path/program.js +``` + +The script path must be **absolute**: the guest resolves paths against the +runner's preopen root (`--dir /`). `--cache-dir` caches the compiled shell. +Everything after `--` goes to the JS shell. Omitting `--night-inprocess` runs +the same binary as a plain interpreter — the differential baseline. + +Step 3 — the full jit-test suite in both lanes: + +``` +python3 js/src/jit-test/jit_test.py -j16 js/src/night/inproc-shell.sh +NIGHT_INPROCESS_OFF=1 python3 js/src/jit-test/jit_test.py -j16 js/src/night/inproc-shell.sh +``` + +Both lanes are expected to pass completely (append a directory like `basic` +to scope). Two directive families keep it that way: + +- `skip-if: nightTierEnabled()` — tests exercising designed-out capability: + the debugger / frame-introspection / interrupt classes, plus an annotated + artifact class (GC-introspection tests sensitive to the tier's + literal-string and allocation profile; each carries an in-file comment). +- Platform skips in the upstream idiom, applying to BOTH lanes of the wasi + shell: `typeof Intl === 'undefined'`, `!this.SharedArrayBuffer`, + `!this.Atomics`, and `getBuildConfiguration("wasi")`. + +## Build-system notes + +- The compiler's dependencies (waffle and the wasm-tools crates) come from + **crates.io**, not from `third_party/rust`: `.cargo/config.toml.in` does + not apply the vendored-source replacement, so `third_party/` carries no + NightMonkey diff. That is a deliberate development override for this fork, + which has no offline-build constraint; see the comment in that file for why + it cannot be scoped to NightMonkey alone. +- `night-compiler` is a workspace member because it is linked into the + `jsrust` staticlib for the in-process lane. +- `nightmonkey` and `wasm-jit-runner` are **standalone cargo projects**, + deliberately outside the workspace: both pull in a wasmtime-class + dependency tree (nightmonkey via wizer) that must not enter the root + `Cargo.lock`. Each is driven by a forced `GENERATED_FILES` step + (`build_nightmonkey.py`, `build_wasm_jit_runner.py`); cargo owns the + incrementality, so a no-op rebuild is subsecond. +- `cargo check` inside `compiler/` typechecks the compiler crate quickly, but + only `./mach build` links it into the wasm shell. + +For performance work, the benchmark-lane configs +(`mozconfig-native`/`-ion`/`-wasm`/`-weval`) live in `configs/` too; +benchmark A/B only with interleaved same-binary runs pinned to one core. + +## Documentation + +- **[`docs/DESIGN.md`](docs/DESIGN.md)** — the design of record: the + soundness model and the object stamp, the BBV emission strategy, the + layered lowerings for the common opcodes, the analysis (data structures, + lattices, abstract interpretation), the runtime ABI, and the known + limitations and rough edges. +- **[`docs/INTEGRATION.md`](docs/INTEGRATION.md)** — everything NightMonkey + touches outside `js/src/night/`, organized by mechanism, with the + stock-build cost of each unconditional change. +- **[`docs/TODO`](docs/TODO)** — the production TODO: holes in completeness + and productionization. + +## Performance + +As of 2026-09-04, comparing to native IonMonkey and baseline tiers, and +against Wasm-hosted interpreter and weval+PBL execution: + +```plain +bench native-ion nat-baseline wasm-interp weval aot aot/wasm-int aot/weval weval/wasm-int ion/weval ion/aot baseline/aot +richards 29205 6489 377 936 11893 31.55 12.71 2.48 31.20 2.46 0.55 +deltablue 28179 6870 395 978 6678 16.91 6.83 2.48 28.81 4.22 1.03 +crypto 42755 5654 714 949 15696 21.98 16.54 1.33 45.05 2.72 0.36 +raytrace 58549 11458 1045 1815 11964 11.45 6.59 1.74 32.26 4.89 0.96 +earley-boyer 83262 21153 1510 3982 15088 9.99 3.79 2.64 20.91 5.52 1.40 +navier-stokes 43926 8269 1223 2090 24980 20.43 11.95 1.71 21.02 1.76 0.33 +splay 29291 23303 5248 6853 10693 2.04 1.56 1.31 4.27 2.74 2.18 +regexp 18601 7223 596 766 2484 4.17 3.24 1.29 24.28 7.49 2.91 +pdfjs 95804 40738 4116 6185 24743 6.01 4.00 1.50 15.49 3.87 1.65 +mandreel 73940 11619 865 1269 19545 22.60 15.40 1.47 58.27 3.78 0.59 +code-load 70224 69259 37108 37005 37271 1.00 1.01 1.00 1.90 1.88 1.86 +box2d 99321 22370 1896 4135 25999 13.71 6.29 2.18 24.02 3.82 0.86 +react-bench 0.631 1.415 15.026 10.421 2.862 5.25 3.64 1.44 16.52 4.54 2.02 +geomean 49233 14111 1527 2569 14247 8.93 5.37 1.66 18.94 3.53 1.05 +(octane = Score higher-better; react-bench = ms/render lower-better; best-of-3, taskset -c 1) +(ratio cols = speedup of A over B, direction-corrected for react-bench; + geomean row: lane cols over octane scores only, ratio cols over all benches) +``` + +We can conclude that NightMonkey is ~9x faster than the Wasm interpreter on +average, or ~5x faster than weval+PBL. It is nearly on par with the native +baseline compiler, and within ~3.5x of the IonMonkey optimized native-code +ceiling (while running within a Wasm engine). On benchmarks where type-based +specialization works especially well, NightMonkey comes within ~2.5x (e.g. +Richards) of native Ion. diff --git a/js/src/night/build_nightmonkey.py b/js/src/night/build_nightmonkey.py new file mode 100644 index 0000000000000..fddd96574a4ee --- /dev/null +++ b/js/src/night/build_nightmonkey.py @@ -0,0 +1,44 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Build the `nightmonkey` AOT snapshot transform with its own cargo project +# and install it into dist/host/bin. Invoked as a forced GENERATED_FILES +# script (see moz.build): cargo owns the incrementality. + +import os +import shutil +import subprocess + +import buildconfig + + +def main(output): + srcdir = os.path.dirname(os.path.abspath(__file__)) + cargo = buildconfig.substs.get("CARGO") or "cargo" + dist_host_bin = os.path.join(buildconfig.topobjdir, "dist", "host", "bin") + os.makedirs(dist_host_bin, exist_ok=True) + + # The enclosing build targets wasm32 and exports target-oriented + # RUSTFLAGS/target selection; this is a HOST tool built with the + # project's own profile, so scrub those knobs and keep the in-crate + # target/ directory (shared with manual `cargo build` invocations). + env = dict(os.environ) + for var in ("RUSTFLAGS", "CARGO_BUILD_TARGET", "CARGO_TARGET_DIR"): + env.pop(var, None) + + projdir = os.path.join(srcdir, "nightmonkey") + subprocess.check_call( + [cargo, "build", "--release", "--features", "wizen"], + cwd=projdir, + env=env, + ) + src = os.path.join(projdir, "target", "release", "nightmonkey") + dst = os.path.join(dist_host_bin, "nightmonkey") + # Copy via a temp name + rename so a concurrently running binary is + # never truncated in place. + tmp = dst + ".tmp" + shutil.copy2(src, tmp) + os.replace(tmp, dst) + + output.write(dst + "\n") diff --git a/js/src/night/build_wasm_jit_runner.py b/js/src/night/build_wasm_jit_runner.py new file mode 100644 index 0000000000000..b012b632ab14e --- /dev/null +++ b/js/src/night/build_wasm_jit_runner.py @@ -0,0 +1,48 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Build the test-only wasm-jit-runner with its own cargo project and +# install it into dist/bin. Invoked as a forced GENERATED_FILES script +# (see moz.build): cargo owns the incrementality. (nightmonkey gets the +# same treatment in build_nightmonkey.py, but is not test-only.) + +import os +import shutil +import subprocess + +import buildconfig + +PROJECTS = [ + ("wasm-jit-runner", "wasm-jit-runner"), +] + + +def main(output): + srcdir = os.path.dirname(os.path.abspath(__file__)) + cargo = buildconfig.substs.get("CARGO") or "cargo" + dist_bin = os.path.join(buildconfig.topobjdir, "dist", "bin") + os.makedirs(dist_bin, exist_ok=True) + + # The enclosing build targets wasm32 and exports target-oriented + # RUSTFLAGS/target selection; these are HOST tools built with each + # project's own profile, so scrub those knobs and keep the in-crate + # target/ directories (shared with manual `cargo build` invocations). + env = dict(os.environ) + for var in ("RUSTFLAGS", "CARGO_BUILD_TARGET", "CARGO_TARGET_DIR"): + env.pop(var, None) + + installed = [] + for project, binary in PROJECTS: + projdir = os.path.join(srcdir, project) + subprocess.check_call([cargo, "build", "--release"], cwd=projdir, env=env) + src = os.path.join(projdir, "target", "release", binary) + dst = os.path.join(dist_bin, binary) + # Copy via a temp name + rename so a concurrently running binary is + # never truncated in place. + tmp = dst + ".tmp" + shutil.copy2(src, tmp) + os.replace(tmp, dst) + installed.append(dst) + + output.write("".join(p + "\n" for p in installed)) diff --git a/js/src/night/compiler/Cargo.toml b/js/src/night/compiler/Cargo.toml new file mode 100644 index 0000000000000..e5ed46f0da998 --- /dev/null +++ b/js/src/night/compiler/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "night-compiler" +version = "0.1.0" +edition = "2021" +rust-version.workspace = true + +[dependencies] +# AOT Wasm emitter: builds the codegen core module and merges it into +# the runtime reactor module (weval-style). +waffle = "0.3.1" +log = "0.4" +# Fast non-SipHash maps: the translator/merge keys are small integers (pcs, +# entity ids), and profile showed SipHash dominating large-input translation. +rustc-hash = "2" +# Blob carving for the in-process batch path; version-matched to waffle's. +wasmparser = { version = "0.248", default-features = false, features = ["std", "validate", "simd"] } +mozilla-central-workspace-hack = { version = "0.1", features = ["night-compiler"], optional = true } + +[dev-dependencies] +wasm-encoder = "0.248" diff --git a/js/src/night/compiler/build.rs b/js/src/night/compiler/build.rs new file mode 100644 index 0000000000000..999946a834916 --- /dev/null +++ b/js/src/night/compiler/build.rs @@ -0,0 +1,349 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +fn main() { + use std::fmt::Write; + + let out_dir = std::env::var("OUT_DIR").unwrap(); + let opcodes_rs = std::path::Path::new(&out_dir).join("opcodes.rs"); + + write_env_regions(&out_dir); + write_region_shape(&out_dir); + + let ops = read_opcodes(); + + let mut out = String::new(); + writeln!( + &mut out, + "#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]" + ) + .unwrap(); + writeln!(&mut out, "#[repr(u16)]").unwrap(); + writeln!(&mut out, "pub enum JSOp {{").unwrap(); + for opcode in &ops { + writeln!(&mut out, " {},", opcode.name).unwrap(); + } + writeln!(&mut out, "}}").unwrap(); + writeln!(&mut out, "#[allow(clippy::len_without_is_empty)]").unwrap(); + writeln!(&mut out, "impl JSOp {{").unwrap(); + writeln!( + &mut out, + " pub fn from_byte(value: u8) -> Option {{" + ) + .unwrap(); + writeln!(&mut out, " match value {{").unwrap(); + for (i, opcode) in ops.iter().enumerate() { + writeln!( + &mut out, + " {} => Some(JSOp::{}),", + i, opcode.name + ) + .unwrap(); + } + for i in ops.len()..256 { + writeln!(&mut out, " {} => None,", i).unwrap(); + } + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, " pub fn len(&self) -> u32 {{").unwrap(); + writeln!(&mut out, " match self {{").unwrap(); + for opcode in &ops { + writeln!( + &mut out, + " JSOp::{} => {},", + opcode.name, opcode.len + ) + .unwrap(); + } + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, " pub fn nuses(&self) -> Option {{").unwrap(); + writeln!(&mut out, " match self {{").unwrap(); + for opcode in &ops { + if opcode.nuses < 0 { + writeln!(&mut out, " JSOp::{} => None,", opcode.name).unwrap(); + } else { + writeln!( + &mut out, + " JSOp::{} => Some({}),", + opcode.name, opcode.nuses + ) + .unwrap(); + } + } + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, " pub fn ndefs(&self) -> u32 {{").unwrap(); + writeln!(&mut out, " match self {{").unwrap(); + for opcode in &ops { + writeln!( + &mut out, + " JSOp::{} => {},", + opcode.name, opcode.ndefs + ) + .unwrap(); + } + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, " }}").unwrap(); + writeln!(&mut out, "}}").unwrap(); + std::fs::write(&opcodes_rs, &out).unwrap(); +} + +fn runtime_header(name: &str) -> String { + let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let path = std::path::Path::new(&dir) + .join("..") + .join("runtime") + .join(name); + println!("cargo:rerun-if-changed={}", path.display()); + std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {}: {e}", path.display())) +} + +// `static constexpr uint32_t = ;` +fn scrape_u32(text: &str, name: &str) -> u32 { + let marker = format!("{name} ="); + let pos = text + .find(&marker) + .unwrap_or_else(|| panic!("{name} not found")); + let rest = &text[pos + marker.len()..]; + let end = rest + .find(';') + .unwrap_or_else(|| panic!("unterminated {name}")); + rest[..end] + .trim() + .parse() + .unwrap_or_else(|e| panic!("bad {name}: {e}")) +} + +/// Generate the region-descriptor mirror from NightEnv.h's NIGHT_ENV_REGIONS +/// X-macro: the same names, the same order, the same wire kinds the engine +/// compiles into `NightEnvDesc`. Both writers (the snapshot tool's +/// `regionTable` and the in-process `env_desc`) fill the generated +/// `RegionWords` struct, so a field added, removed or renamed in the header +/// is a Rust *compile* error at every writer, not a runtime surprise. +fn write_env_regions(out_dir: &str) { + use std::fmt::Write; + + let env_h = runtime_header("NightEnv.h"); + let reg_h = runtime_header("NightRegistration.h"); + let abi_version = scrape_u32(®_h, "NightAotAbiVersion"); + let header_words = scrape_u32(&env_h, "NightEnvDescHeaderWords"); + + let start = env_h + .find("#define NIGHT_ENV_REGIONS(_)") + .expect("NIGHT_ENV_REGIONS not found"); + let mut body = String::new(); + for line in env_h[start..].lines() { + body.push_str(line); + body.push('\n'); + if !line.trim_end().ends_with('\\') { + break; + } + } + let mut regions: Vec<(String, String)> = Vec::new(); + let mut rest = body.as_str(); + while let Some(pos) = rest.find("_(") { + rest = &rest[pos + 2..]; + let Some((args, tail)) = rest.split_once(')') else { + break; + }; + rest = tail; + let Some((name, kind)) = args.split_once(',') else { + continue; + }; + let (name, kind) = (name.trim(), kind.trim()); + if name.is_empty() || !matches!(kind, "Table" | "Len" | "Addr") { + continue; + } + regions.push((name.to_string(), kind.to_string())); + } + assert!( + regions.len() > 10, + "suspiciously few NIGHT_ENV_REGIONS entries parsed" + ); + + let mut out = String::new(); + writeln!(out, "// Generated from NightEnv.h NIGHT_ENV_REGIONS.").unwrap(); + writeln!(out, "pub const ABI_VERSION: u32 = {abi_version};").unwrap(); + writeln!( + out, + "pub const ENV_DESC_HEADER_WORDS: usize = {header_words};" + ) + .unwrap(); + writeln!(out, "pub const REGION_COUNT: usize = {};", regions.len()).unwrap(); + writeln!(out, "#[derive(Clone, Copy, Debug, PartialEq, Eq)]").unwrap(); + writeln!(out, "pub enum RegionKind {{").unwrap(); + writeln!(out, " Table,").unwrap(); + writeln!(out, " Len,").unwrap(); + writeln!(out, " Addr,").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!( + out, + "pub const REGION_KINDS: [RegionKind; REGION_COUNT] = [" + ) + .unwrap(); + for (_, kind) in ®ions { + writeln!(out, " RegionKind::{kind},").unwrap(); + } + writeln!(out, "];").unwrap(); + writeln!(out, "pub const REGION_NAMES: [&str; REGION_COUNT] = [").unwrap(); + for (name, _) in ®ions { + writeln!(out, " \"{name}\",").unwrap(); + } + writeln!(out, "];").unwrap(); + writeln!( + out, + "/// The region words, by name. `to_words` orders them for the wire." + ) + .unwrap(); + writeln!(out, "#[allow(non_snake_case)]").unwrap(); + writeln!(out, "#[derive(Clone, Copy, Debug, Default)]").unwrap(); + writeln!(out, "pub struct RegionWords {{").unwrap(); + for (name, _) in ®ions { + writeln!(out, " pub {name}: u32,").unwrap(); + } + writeln!(out, "}}").unwrap(); + writeln!(out, "impl RegionWords {{").unwrap(); + writeln!(out, " pub fn to_words(&self) -> [u32; REGION_COUNT] {{").unwrap(); + writeln!(out, " [").unwrap(); + for (name, _) in ®ions { + writeln!(out, " self.{name},").unwrap(); + } + writeln!(out, " ]").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + std::fs::write(std::path::Path::new(out_dir).join("env_regions.rs"), out).unwrap(); +} + +/// Generate the region-shape mirror from NightRegionShape.h's +/// NIGHT_REGION_SHAPE X-macro: every entry stride, table size and +/// intra-region offset the compiled code and the runtime both index with. +/// One literal with two generated consumers, so the two sides' copies of a +/// layout constant cannot drift apart -- the one silent-miscompile class +/// the tier's guards cannot cover. +fn write_region_shape(out_dir: &str) { + use std::fmt::Write; + + let text = runtime_header("NightRegionShape.h"); + let start = text + .find("#define NIGHT_REGION_SHAPE(_)") + .expect("NIGHT_REGION_SHAPE not found"); + let mut body = String::new(); + for line in text[start..].lines() { + body.push_str(line); + body.push('\n'); + if !line.trim_end().ends_with('\\') { + break; + } + } + // Comment lines inside the macro body also contain "_(" -free text, but a + // `/* ... */` run could in principle hold one; strip comments first so the + // parse sees only entries. + let mut stripped = String::new(); + let mut rest = body.as_str(); + while let Some(pos) = rest.find("/*") { + stripped.push_str(&rest[..pos]); + match rest[pos..].find("*/") { + Some(end) => rest = &rest[pos + end + 2..], + None => { + rest = ""; + break; + } + } + } + stripped.push_str(rest); + + let mut entries: Vec<(String, u32)> = Vec::new(); + let mut rest = stripped.as_str(); + while let Some(pos) = rest.find("_(") { + rest = &rest[pos + 2..]; + let Some((args, tail)) = rest.split_once(')') else { + break; + }; + rest = tail; + let Some((name, value)) = args.split_once(',') else { + continue; + }; + let name = name.trim(); + let value: u32 = value + .trim() + .parse() + .unwrap_or_else(|e| panic!("NIGHT_REGION_SHAPE {name} is not a literal: {e}")); + assert!(!name.is_empty()); + entries.push((name.to_string(), value)); + } + assert!( + entries.len() > 20, + "suspiciously few NIGHT_REGION_SHAPE entries parsed" + ); + + let mut out = String::new(); + writeln!( + out, + "// Generated from NightRegionShape.h NIGHT_REGION_SHAPE." + ) + .unwrap(); + for (name, value) in &entries { + writeln!(out, "pub const {}: u32 = {value};", screaming(name)).unwrap(); + } + std::fs::write(std::path::Path::new(out_dir).join("region_shape.rs"), out).unwrap(); +} + +/// `inlineIcWayBytes` -> `INLINE_IC_WAY_BYTES`. +fn screaming(name: &str) -> String { + let mut out = String::new(); + for c in name.chars() { + if c.is_ascii_uppercase() && !out.is_empty() { + out.push('_'); + } + out.push(c.to_ascii_uppercase()); + } + out +} + +struct Opcode { + name: String, + len: u32, + nuses: i32, + ndefs: u32, +} + +fn read_opcodes() -> Vec { + let path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let path = std::path::Path::new(&path) + .join("..") + .join("..") + .join("vm") + .join("Opcodes.h"); + let opcodes_file = std::fs::read(&path).unwrap(); + let opcodes_file = std::str::from_utf8(&opcodes_file).unwrap(); + + let mut in_opcodes = false; + let mut ret = vec![]; + for line in opcodes_file.lines() { + if !in_opcodes && line.contains("#define FOR_EACH_OPCODE") { + in_opcodes = true; + } else if line.contains("FOR_EACH_TRAILING_UNUSED_OPCODE") { + break; + } else if in_opcodes { + if let Some(start) = line.find("MACRO(") { + let start = start + 6; + let end = line.find(")").unwrap(); + let fields = &line[start..end]; + let split = fields.split(",").collect::>(); + let name = split[0].trim().to_string(); + let len = split[3].trim().parse::().unwrap(); + let nuses = split[4].trim().parse::().unwrap(); + let ndefs = split[5].trim().parse::().unwrap(); + ret.push(Opcode { + name, + len, + nuses, + ndefs, + }); + } + } + } + ret +} diff --git a/js/src/night/compiler/night-compiler.h b/js/src/night/compiler/night-compiler.h new file mode 100644 index 0000000000000..b5f613f82a7ba --- /dev/null +++ b/js/src/night/compiler/night-compiler.h @@ -0,0 +1,230 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef js_night_compiler_night_compiler_h +#define js_night_compiler_night_compiler_h + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void night_source_t; +typedef uint32_t night_source_object_t; +const night_source_object_t NIGHT_SOURCE_OTHER = UINT32_MAX; + +const uint8_t NIGHT_OBJECT_KIND_OTHER = 0; +const uint8_t NIGHT_OBJECT_KIND_PLAIN = 1; +const uint8_t NIGHT_OBJECT_KIND_ARRAY = 2; +const uint8_t NIGHT_OBJECT_KIND_FUNCTION = 3; + +// Primordial-identity overlay ids (live-heap ingestion): a live +// object recognized as one of these is mapped to the described builtin +// abstraction via night_source_object_set_builtin_id. Must match +// BuiltinId::from_u8 in analysis/builtins.rs. +const uint8_t NIGHT_BUILTIN_OBJECT_CTOR = 0; +const uint8_t NIGHT_BUILTIN_OBJECT_PROTO = 1; +const uint8_t NIGHT_BUILTIN_ARRAY_CTOR = 2; +const uint8_t NIGHT_BUILTIN_ARRAY_PROTO = 3; +const uint8_t NIGHT_BUILTIN_FUNCTION_CTOR = 4; +const uint8_t NIGHT_BUILTIN_FUNCTION_PROTO = 5; +const uint8_t NIGHT_BUILTIN_STRING_CTOR = 6; +const uint8_t NIGHT_BUILTIN_STRING_PROTO = 7; +const uint8_t NIGHT_BUILTIN_NUMBER_CTOR = 8; +const uint8_t NIGHT_BUILTIN_NUMBER_PROTO = 9; +const uint8_t NIGHT_BUILTIN_BOOLEAN_CTOR = 10; +const uint8_t NIGHT_BUILTIN_BOOLEAN_PROTO = 11; +const uint8_t NIGHT_BUILTIN_MATH = 12; +const uint8_t NIGHT_BUILTIN_DATE_CTOR = 13; +const uint8_t NIGHT_BUILTIN_DATE_PROTO = 14; +const uint8_t NIGHT_BUILTIN_ERROR_CTOR = 15; +const uint8_t NIGHT_BUILTIN_ERROR_PROTO = 16; +const uint8_t NIGHT_BUILTIN_REGEXP_CTOR = 17; +const uint8_t NIGHT_BUILTIN_REGEXP_PROTO = 18; +const uint8_t NIGHT_BUILTIN_PRINT = 19; +const uint8_t NIGHT_BUILTIN_ASSERT_EQ = 20; +const uint8_t NIGHT_BUILTIN_NONE = 0xff; + +night_source_t* night_source_new(); + +// Write the deterministic textual dump of the Source graph (rooted at +// `root`) to `path`. Returns false on I/O failure. +bool night_source_dump_file(night_source_t* source, night_source_object_t root, + const char* path); +void night_source_delete(night_source_t* source); +night_source_object_t night_source_add_object(night_source_t* source); +night_source_object_t night_source_add_script(night_source_t* source, + const uint8_t* bytecode, + uint32_t len); +void night_source_mark_selfhosted(night_source_t* source, + night_source_object_t script, + const uint8_t* name, uint32_t name_len); +void night_source_add_regex_program( + night_source_t* source, const uint16_t* pattern_chars, uint32_t pattern_len, + uint32_t flags, const uint8_t* latin1_bc, uint32_t latin1_len, + const uint8_t* twobyte_bc, uint32_t twobyte_len, uint32_t num_registers, + uint32_t pair_count); +night_source_object_t night_source_add_string_latin1(night_source_t* source, + const uint8_t* bytes, + uint32_t len); +night_source_object_t night_source_add_string_wide(night_source_t* source, + const uint16_t* codepoints, + uint32_t len); +night_source_object_t night_source_add_undefined(night_source_t* source); +night_source_object_t night_source_add_null(night_source_t* source); +night_source_object_t night_source_add_boolean(night_source_t* source, + bool value); +night_source_object_t night_source_add_int32(night_source_t* source, + int32_t value); +night_source_object_t night_source_add_double(night_source_t* source, + double value); +void night_source_object_set_non_native(night_source_t* source, + night_source_object_t obj); +void night_source_object_set_kind(night_source_t* source, + night_source_object_t obj, uint8_t kind); +void night_source_object_set_name(night_source_t* source, + night_source_object_t obj, + night_source_object_t name); +void night_source_object_set_script(night_source_t* source, + night_source_object_t obj, + night_source_object_t script); +// Record an object's concrete [[Prototype]] (live-heap ingestion). Only +// non-primordial protos are passed; primordial/null protos are left +// unset so the analysis synthesizes the proto from the object's kind. +void night_source_object_set_proto(night_source_t* source, + night_source_object_t obj, + night_source_object_t proto); +// Mark a live object as a recognized primordial (identity overlay), +// keyed by a NIGHT_BUILTIN_* id: transcription reuses the +// described builtin abstraction instead of transcribing it. +void night_source_object_set_builtin_id(night_source_t* source, + night_source_object_t obj, + uint8_t builtin_id); +// Mark a live object as the global object (global-from-live): its +// own properties seed the Global(name) bindings. +void night_source_set_global_object(night_source_t* source, + night_source_object_t obj); +void night_source_object_add_property(night_source_t* source, + night_source_object_t obj, + night_source_object_t key, + night_source_object_t value); +void night_source_object_add_element(night_source_t* source, + night_source_object_t obj, uint32_t index, + night_source_object_t value); +void night_source_script_add_gcthing(night_source_t* source, + night_source_object_t script, + night_source_object_t value); +void night_source_script_set_resume_offsets(night_source_t* source, + night_source_object_t script, + const uint32_t* offsets, + uint32_t len); +void night_source_script_add_try_note(night_source_t* source, + night_source_object_t script, + uint8_t kind, uint32_t stack_depth, + uint32_t start, uint32_t length); +night_source_object_t night_source_add_scope(night_source_t* source, + uint8_t kind, + bool has_environment); +// Record a binding declared in a scope: its name (a string source +// object), whether it is a `var` binding (vs lexical/formal/...), +// and its environment slot if it is closed-over. +void night_source_scope_add_binding(night_source_t* source, + night_source_object_t scope, + night_source_object_t name, bool is_var, + bool has_env_slot, uint32_t env_slot); +// Mark a scope as a (Strict)NamedLambda scope (holds a named function +// expression's self-name binding, initialized by the VM). +void night_source_scope_set_is_named_lambda(night_source_t* source, + night_source_object_t scope); +// Record a concrete (env slot, value) pair read from a live CallObject +// captured by the post-setup snapshot (live-heap ingestion). Seeds the +// scope's environment abstraction so steady-state GetAliasedVar resolves +// to the captured value. +void night_source_scope_add_env_slot_value(night_source_t* source, + night_source_object_t scope, + uint32_t slot, + night_source_object_t value); +void night_source_scope_set_enclosing(night_source_t* source, + night_source_object_t scope, + night_source_object_t enclosing); +void night_source_script_add_scope_note(night_source_t* source, + night_source_object_t script, + uint32_t gcthing_index, uint32_t start, + uint32_t length); +// Set a script's declared formal-argument count (0 for non-function +// scripts); used to seed the arguments of functions callable from +// outside the closed world. +void night_source_script_set_nargs(night_source_t* source, + night_source_object_t script, + uint16_t nargs); +// Set whether the script is a generator or async function (its call +// result is a VM-created generator/promise object, not its return +// value). +void night_source_script_set_is_generator_or_async(night_source_t* source, + night_source_object_t script, + bool value); +// Set the script's strictness flags: strict-mode code, and whether it +// gets a MAPPED arguments object (sloppy + simple formals + uses +// `arguments`; such scripts stay interpreted). +void night_source_script_set_strictness(night_source_t* source, + night_source_object_t script, + bool strict, bool has_mapped_args); +void night_source_script_set_body_scope(night_source_t* source, + night_source_object_t script, + night_source_object_t scope); + +// In-process AOT batch build. Compiles the Source graph into wasm-jit-runner +// function blobs (blob i is predicted at funcref-table index table_base + i) +// plus a serialized environment descriptor. +// +// Helper signature strings ("i(ii)" style): "()", one char per +// type: +// i = i32 (pointers, uint32_t/int32_t, bool) +// j = i64 (uint64_t, boxed JS Values) +// f = f32 +// d = f64 (double) +// v = void (return position only) +// Examples: "i(iijj)" is int32_t f(int32_t, int32_t, uint64_t, uint64_t); +// "v(i)" is void f(int32_t). +// +// `alloc` is called exactly twice (fixed layout region, then the prop-IC/ +// cell region + string-literal blob) and must return zeroed (calloc-style), +// 8-aligned, non-null memory; it may be called with size 0. The env +// descriptor is a 29-word little-endian u32 header followed by the +// serialized atom/gbind/layout/fuse/regex/strlit tables (offsets into the +// descriptor buffer; region addresses point into the `alloc` regions); the +// header word order is documented in wasm/inprocess.rs (ENV_DESC_WORDS). +// The string-literal payload at [strlit_off, strlit_off+strlit_len) must be +// copied to linear address strlit_addr before compiled code runs. +typedef uint32_t (*night_alloc_fn)(size_t size); +typedef void night_inproc_out_t; +night_inproc_out_t* night_inproc_build(night_source_t* analysis_source, + night_source_object_t root_id, + const char* const* helper_names, + const char* const* helper_sigs, + const uint32_t* helper_funcptrs, + uint32_t n_helpers, uint32_t table_base, + night_alloc_fn alloc); +uint32_t night_inproc_num_blobs(night_inproc_out_t* out); +const uint8_t* night_inproc_blob_ptr(night_inproc_out_t* out, uint32_t i); +uint32_t night_inproc_blob_len(night_inproc_out_t* out, uint32_t i); +uint32_t night_inproc_num_externs(night_inproc_out_t* out); +const uint32_t* night_inproc_extern_indices(night_inproc_out_t* out); +uint32_t night_inproc_num_scripts(night_inproc_out_t* out); +uint32_t night_inproc_script_source_id(night_inproc_out_t* out, uint32_t i); +uint32_t night_inproc_script_blob(night_inproc_out_t* out, uint32_t i); +const uint8_t* night_inproc_env_desc_ptr(night_inproc_out_t* out); +uint32_t night_inproc_env_desc_len(night_inproc_out_t* out); +void night_inproc_delete(night_inproc_out_t* out); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // js_night_compiler_night_compiler_h diff --git a/js/src/night/compiler/src/bytecode.rs b/js/src/night/compiler/src/bytecode.rs new file mode 100644 index 0000000000000..f5128ef5bd65f --- /dev/null +++ b/js/src/night/compiler/src/bytecode.rs @@ -0,0 +1,1082 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::ids::Pc; +use crate::source::SourceObjectId; + +mod opcodes { + include!(concat!(env!("OUT_DIR"), "/opcodes.rs")); +} +pub use opcodes::JSOp; + +/// Mirrors C++ `TryNoteKind` (js/src/vm/StencilEnums.h). +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TryNoteKind { + Catch = 0, + Finally = 1, + ForIn = 2, + Destructuring = 3, + ForOf = 4, + ForOfIterClose = 5, + Loop = 6, +} + +#[derive(Clone, Copy, Debug)] +pub struct TryNote { + pub kind: TryNoteKind, + pub stack_depth: u32, + pub start: Pc, + pub length: u32, +} + +/// Mirrors C++ `ScopeNote` (js/src/vm/SharedStencil.h): the static +/// scope covering a bytecode range. +#[derive(Clone, Copy, Debug)] +pub struct ScopeNote { + /// Index of the scope in the script's gcthings, or u32::MAX for + /// "no block scope in this range" (the body scope applies). + pub gcthing_index: u32, + pub start: Pc, + pub length: u32, +} + +#[derive(Debug)] +pub struct Script { + pub bytecode: Vec, + /// Runtime `JSScript*` cell address (0 when unavailable, e.g. sources + /// built without a live heap). Stable once read: compaction is disabled. + pub addr: u32, + pub gcthings: Vec, + pub resume_offsets: Vec, + pub try_notes: Vec, + pub scope_notes: Vec, + /// The script's outermost (body) scope: the static scope at any + /// PC not covered by a scope note. + pub body_scope: Option, + /// Declared formal-argument count (0 for non-function scripts). + pub nargs: u16, + /// Whether the script is a generator or async function: calling + /// it returns a VM-created generator/promise object, not its Ret + /// value. + pub is_generator_or_async: bool, + /// Whether the script is a class constructor body: it has no `[[Call]]` + /// (a call must throw, only constructs run it), so call-site direct + /// dispatch (splice / fuse arm) must exclude it. + pub is_class_ctor: bool, + /// Whether the script is strict-mode code. Sloppy scripts need the + /// `FunctionThis` boxing diamond (null/undefined -> global, + /// primitives -> wrappers). + pub strict: bool, + /// Whether the script gets a mapped arguments object (sloppy, simple + /// formals, uses `arguments`): writes through `arguments[i]` alias the + /// formals. The lazy-args machinery builds only the unmapped flavor, + /// so such scripts stay interpreted (capability gate). + pub has_mapped_args: bool, +} + +impl Script { + pub fn parser(&self) -> BytecodeParser<'_> { + // Try notes are stored in emission-completion order (inner + // notes first), not by start offset; sort so the parser can + // surface them as their start PC is reached. + let mut try_notes = self.try_notes.clone(); + try_notes.sort_by_key(|n| n.start); + BytecodeParser { + pc: Pc::new(0), + data: &self.bytecode[..], + try_notes, + try_note_idx: 0, + resume_offsets: &self.resume_offsets[..], + } + } +} + +pub struct BytecodeParser<'a> { + pc: Pc, + data: &'a [u8], + try_notes: Vec, + try_note_idx: usize, + resume_offsets: &'a [Pc], +} + +impl<'a> BytecodeParser<'a> { + /// Number of unconsumed bytes remaining (used to measure how many bytes + /// an op consumed). + pub fn remaining(&self) -> usize { + self.data.len() + } + + pub fn read_byte(&mut self) -> Option { + if self.data.is_empty() { + return None; + } + let byte = self.data[0]; + self.data = &self.data[1..]; + self.pc += 1; + Some(byte) + } + + pub fn next_op(&mut self) -> Option { + let byte = self.read_byte()?; + Some(JSOp::from_byte(byte).expect("Invalid bytecode")) + } + + pub fn try_note_at_pc(&mut self) -> Option { + // Multiple notes can start at the same PC (e.g. a for-of + // loop's ForOf and iterator-close ranges); callers loop until + // None. + if self.try_note_idx < self.try_notes.len() + && self.try_notes[self.try_note_idx].start <= self.pc + { + let ret = self.try_notes[self.try_note_idx]; + self.try_note_idx += 1; + Some(ret) + } else { + None + } + } + + pub fn next_uint8(&mut self) -> Option { + self.read_byte() + } + + pub fn next_uint16(&mut self) -> Option { + let a = u16::from(self.read_byte()?); + let b = u16::from(self.read_byte()?); + Some(a | (b << 8)) + } + + pub fn peek_uint16(&self) -> Option { + if self.data.len() < 2 { + None + } else { + Some(u16::from_le_bytes([self.data[0], self.data[1]])) + } + } + + pub fn next_uint24(&mut self) -> Option { + let a = u32::from(self.read_byte()?); + let b = u32::from(self.read_byte()?); + let c = u32::from(self.read_byte()?); + Some(a | (b << 8) | (c << 16)) + } + + pub fn next_uint32(&mut self) -> Option { + let a = u32::from(self.read_byte()?); + let b = u32::from(self.read_byte()?); + let c = u32::from(self.read_byte()?); + let d = u32::from(self.read_byte()?); + Some(a | (b << 8) | (c << 16) | (d << 24)) + } + + pub fn next_uint64(&mut self) -> Option { + let a = u64::from(self.next_uint32()?); + let b = u64::from(self.next_uint32()?); + Some(a | (b << 32)) + } + + pub fn advance(&mut self, len: usize) -> Option<()> { + if len > self.data.len() { + return None; + } + self.data = &self.data[len..]; + self.pc += u32::try_from(len).unwrap(); + Some(()) + } + + pub fn opcodes<'b>(&'b mut self) -> impl Iterator + 'b + where + 'b: 'a, + { + std::iter::from_fn(|| { + let op = self.next_op()?; + self.advance(usize::try_from(op.len()).unwrap() - 1)?; + Some(op) + }) + } + + pub fn next_int8(&mut self) -> Option { + Some(self.read_byte()? as i8) + } + + pub fn next_int32(&mut self) -> Option { + Some(self.next_uint32()? as i32) + } + + pub fn visit(mut self, mut visitor: V) -> V { + let mut pc = 0u32; + loop { + let before = self.data.len(); + let Some(op) = self.next_op() else { break }; + let nuses = match op { + // Call and variants: callee, this, args[0..argc] + JSOp::Call + | JSOp::CallContent + | JSOp::CallIter + | JSOp::CallContentIter + | JSOp::CallIgnoresRv + | JSOp::Eval + | JSOp::StrictEval => u32::from(self.peek_uint16().unwrap()) + 2, + // New and variants: callee, isConstructing, args[0..argc], newTarget + JSOp::New | JSOp::NewContent | JSOp::SuperCall => { + u32::from(self.peek_uint16().unwrap()) + 3 + } + // PopN: discarded[0..n] + JSOp::PopN => u32::from(self.peek_uint16().unwrap()), + // All others are fixed-use-count. + _ => op.nuses().unwrap(), + }; + let ndefs = op.ndefs(); + visitor.before_op( + Pc::new(pc), + op, + usize::try_from(nuses).unwrap(), + usize::try_from(ndefs).unwrap(), + ); + match op { + JSOp::Undefined => visitor.undefined(), + JSOp::Null => visitor.null(), + JSOp::False => visitor.false_(), + JSOp::True => visitor.true_(), + JSOp::Int32 => { + let value = self.next_uint32().unwrap(); + visitor.int32(value); + } + JSOp::Zero => visitor.zero(), + JSOp::One => visitor.one(), + JSOp::Int8 => { + let value = self.next_uint8().unwrap(); + visitor.int8(value); + } + JSOp::Uint16 => { + let value = self.next_uint16().unwrap(); + visitor.uint16(value); + } + JSOp::Uint24 => { + let value = self.next_uint24().unwrap(); + visitor.uint24(value); + } + JSOp::Double => { + let value = self.next_uint64().unwrap(); + visitor.double(value); + } + JSOp::BigInt => { + let bigint_index = self.next_uint32().unwrap(); + visitor.bigint(bigint_index); + } + JSOp::String => { + let atom_index = self.next_uint32().unwrap(); + visitor.string(atom_index); + } + JSOp::Symbol => { + let code = self.next_uint8().unwrap(); + visitor.symbol(code); + } + JSOp::Void => visitor.void(), + JSOp::Typeof => visitor.typeof_(), + JSOp::TypeofExpr => visitor.typeof_expr(), + JSOp::TypeofEq => { + let operand = self.next_uint8().unwrap(); + visitor.typeof_eq(operand); + } + JSOp::Pos => visitor.pos(), + JSOp::Neg => visitor.neg(), + JSOp::BitNot => visitor.bit_not(), + JSOp::Not => visitor.not_(), + JSOp::BitOr => visitor.bit_or(), + JSOp::BitXor => visitor.bit_xor(), + JSOp::BitAnd => visitor.bit_and(), + JSOp::Eq => visitor.eq(), + JSOp::Ne => visitor.ne(), + JSOp::StrictEq => visitor.strict_eq(), + JSOp::StrictNe => visitor.strict_ne(), + JSOp::StrictConstantEq => { + let operand = self.next_uint16().unwrap(); + visitor.strict_constant_eq(operand); + } + JSOp::StrictConstantNe => { + let operand = self.next_uint16().unwrap(); + visitor.strict_constant_ne(operand); + } + JSOp::Lt => visitor.lt(), + JSOp::Gt => visitor.gt(), + JSOp::Le => visitor.le(), + JSOp::Ge => visitor.ge(), + JSOp::Instanceof => visitor.instanceof(), + JSOp::In => visitor.in_(), + JSOp::Lsh => visitor.lsh(), + JSOp::Rsh => visitor.rsh(), + JSOp::Ursh => visitor.ursh(), + JSOp::Add => visitor.add(), + JSOp::Sub => visitor.sub(), + JSOp::Inc => visitor.inc(), + JSOp::Dec => visitor.dec(), + JSOp::Mul => visitor.mul(), + JSOp::Div => visitor.div(), + JSOp::Mod => visitor.mod_(), + JSOp::Pow => visitor.pow(), + JSOp::NopIsAssignOp => visitor.nop_is_assign_op(), + JSOp::ToPropertyKey => visitor.to_property_key(), + JSOp::ToNumeric => visitor.to_numeric(), + JSOp::ToString => visitor.to_string(), + JSOp::IsNullOrUndefined => visitor.is_null_or_undefined(), + JSOp::GlobalThis => visitor.global_this(), + JSOp::NonSyntacticGlobalThis => visitor.non_syntactic_global_this(), + JSOp::NewTarget => visitor.new_target(), + JSOp::DynamicImport => visitor.dynamic_import(), + JSOp::ImportMeta => visitor.import_meta(), + JSOp::NewInit => { + let property_count = self.next_uint8().unwrap(); + visitor.new_init(property_count); + } + JSOp::NewObject => { + let shape_index = self.next_uint32().unwrap(); + visitor.new_object(shape_index); + } + JSOp::Object => { + let object_index = self.next_uint32().unwrap(); + visitor.object(object_index); + } + JSOp::ObjWithProto => visitor.obj_with_proto(), + JSOp::InitProp => { + let name_index = self.next_uint32().unwrap(); + visitor.init_prop(name_index); + } + JSOp::InitHiddenProp => { + let name_index = self.next_uint32().unwrap(); + visitor.init_hidden_prop(name_index); + } + JSOp::InitLockedProp => { + let name_index = self.next_uint32().unwrap(); + visitor.init_locked_prop(name_index); + } + JSOp::InitElem => visitor.init_elem(), + JSOp::InitHiddenElem => visitor.init_hidden_elem(), + JSOp::InitLockedElem => visitor.init_locked_elem(), + JSOp::InitPropGetter => { + let name_index = self.next_uint32().unwrap(); + visitor.init_prop_getter(name_index); + } + JSOp::InitHiddenPropGetter => { + let name_index = self.next_uint32().unwrap(); + visitor.init_hidden_prop_getter(name_index); + } + JSOp::InitElemGetter => visitor.init_elem_getter(), + JSOp::InitHiddenElemGetter => visitor.init_hidden_elem_getter(), + JSOp::InitPropSetter => { + let name_index = self.next_uint32().unwrap(); + visitor.init_prop_setter(name_index); + } + JSOp::InitHiddenPropSetter => { + let name_index = self.next_uint32().unwrap(); + visitor.init_hidden_prop_setter(name_index); + } + JSOp::InitElemSetter => visitor.init_elem_setter(), + JSOp::InitHiddenElemSetter => visitor.init_hidden_elem_setter(), + JSOp::GetProp => { + let name_index = self.next_uint32().unwrap(); + visitor.get_prop(name_index); + } + JSOp::GetElem => visitor.get_elem(), + JSOp::SetProp => { + let name_index = self.next_uint32().unwrap(); + visitor.set_prop(name_index); + } + JSOp::StrictSetProp => { + let name_index = self.next_uint32().unwrap(); + visitor.strict_set_prop(name_index); + } + JSOp::SetElem => visitor.set_elem(), + JSOp::StrictSetElem => visitor.strict_set_elem(), + JSOp::DelProp => { + let name_index = self.next_uint32().unwrap(); + visitor.del_prop(name_index); + } + JSOp::StrictDelProp => { + let name_index = self.next_uint32().unwrap(); + visitor.strict_del_prop(name_index); + } + JSOp::DelElem => visitor.del_elem(), + JSOp::StrictDelElem => visitor.strict_del_elem(), + JSOp::HasOwn => visitor.has_own(), + JSOp::CheckPrivateField => { + let throw_condition = self.next_uint8().unwrap(); + let msg_kind = self.next_uint8().unwrap(); + visitor.check_private_field(throw_condition, msg_kind); + } + JSOp::NewPrivateName => { + let name_index = self.next_uint32().unwrap(); + visitor.new_private_name(name_index); + } + JSOp::SuperBase => visitor.super_base(), + JSOp::GetPropSuper => { + let name_index = self.next_uint32().unwrap(); + visitor.get_prop_super(name_index); + } + JSOp::GetElemSuper => visitor.get_elem_super(), + JSOp::SetPropSuper => { + let name_index = self.next_uint32().unwrap(); + visitor.set_prop_super(name_index); + } + JSOp::StrictSetPropSuper => { + let name_index = self.next_uint32().unwrap(); + visitor.strict_set_prop_super(name_index); + } + JSOp::SetElemSuper => visitor.set_elem_super(), + JSOp::StrictSetElemSuper => visitor.strict_set_elem_super(), + JSOp::Iter => visitor.iter(), + JSOp::MoreIter => visitor.more_iter(), + JSOp::IsNoIter => visitor.is_no_iter(), + JSOp::EndIter => visitor.end_iter(), + JSOp::CloseIter => { + let kind = self.next_uint8().unwrap(); + visitor.close_iter(kind); + } + JSOp::OptimizeGetIterator => visitor.optimize_get_iterator(), + JSOp::CheckIsObj => { + let kind = self.next_uint8().unwrap(); + visitor.check_is_obj(kind); + } + JSOp::CheckObjCoercible => visitor.check_obj_coercible(), + JSOp::ToAsyncIter => visitor.to_async_iter(), + JSOp::MutateProto => visitor.mutate_proto(), + JSOp::NewArray => { + let length = self.next_uint32().unwrap(); + visitor.new_array(length); + } + JSOp::InitElemArray => { + let index = self.next_uint32().unwrap(); + visitor.init_elem_array(index); + } + JSOp::InitElemInc => visitor.init_elem_inc(), + JSOp::Hole => visitor.hole(), + JSOp::RegExp => { + let regexp_index = self.next_uint32().unwrap(); + visitor.reg_exp(regexp_index); + } + JSOp::Lambda => { + let func_index = self.next_uint32().unwrap(); + visitor.lambda(func_index); + } + JSOp::SetFunName => { + let prefix_kind = self.next_uint8().unwrap(); + visitor.set_fun_name(prefix_kind); + } + JSOp::InitHomeObject => visitor.init_home_object(), + JSOp::CheckClassHeritage => visitor.check_class_heritage(), + JSOp::FunWithProto => { + let func_index = self.next_uint32().unwrap(); + visitor.fun_with_proto(func_index); + } + JSOp::BuiltinObject => { + let kind = self.next_uint8().unwrap(); + visitor.builtin_object(kind); + } + JSOp::Call => { + let argc = self.next_uint16().unwrap(); + visitor.call(argc); + } + JSOp::CallContent => { + let argc = self.next_uint16().unwrap(); + visitor.call_content(argc); + } + JSOp::CallIter => { + let argc = self.next_uint16().unwrap(); + visitor.call_iter(argc); + } + JSOp::CallContentIter => { + let argc = self.next_uint16().unwrap(); + visitor.call_content_iter(argc); + } + JSOp::CallIgnoresRv => { + let argc = self.next_uint16().unwrap(); + visitor.call_ignores_rv(argc); + } + JSOp::SpreadCall => visitor.spread_call(), + JSOp::OptimizeSpreadCall => visitor.optimize_spread_call(), + JSOp::Eval => { + let argc = self.next_uint16().unwrap(); + visitor.eval(argc); + } + JSOp::SpreadEval => visitor.spread_eval(), + JSOp::StrictEval => { + let argc = self.next_uint16().unwrap(); + visitor.strict_eval(argc); + } + JSOp::StrictSpreadEval => visitor.strict_spread_eval(), + JSOp::ImplicitThis => visitor.implicit_this(), + JSOp::CallSiteObj => { + let object_index = self.next_uint32().unwrap(); + visitor.call_site_obj(object_index); + } + JSOp::IsConstructing => visitor.is_constructing(), + JSOp::New => { + let argc = self.next_uint16().unwrap(); + visitor.new_(argc); + } + JSOp::NewContent => { + let argc = self.next_uint16().unwrap(); + visitor.new_content(argc); + } + JSOp::SuperCall => { + let argc = self.next_uint16().unwrap(); + visitor.super_call(argc); + } + JSOp::SpreadNew => visitor.spread_new(), + JSOp::SpreadSuperCall => visitor.spread_super_call(), + JSOp::SuperFun => visitor.super_fun(), + JSOp::CheckThisReinit => visitor.check_this_reinit(), + JSOp::Generator => visitor.generator(), + JSOp::InitialYield => { + let resume_index = self.next_uint24().unwrap(); + visitor.initial_yield(resume_index); + } + JSOp::AfterYield => { + let ic_index = self.next_uint32().unwrap(); + visitor.after_yield(ic_index); + } + JSOp::FinalYieldRval => visitor.final_yield_rval(), + JSOp::Yield => { + let resume_index = self.next_uint24().unwrap(); + visitor.yield_(resume_index); + } + JSOp::IsGenClosing => visitor.is_gen_closing(), + JSOp::AsyncAwait => visitor.async_await(), + JSOp::AsyncResolve => visitor.async_resolve(), + JSOp::AsyncReject => visitor.async_reject(), + JSOp::Await => { + let resume_index = self.next_uint24().unwrap(); + visitor.await_(resume_index); + } + JSOp::CanSkipAwait => visitor.can_skip_await(), + JSOp::MaybeExtractAwaitValue => visitor.maybe_extract_await_value(), + JSOp::ResumeKind => { + let resume_kind = self.next_uint8().unwrap(); + visitor.resume_kind(resume_kind); + } + JSOp::CheckResumeKind => visitor.check_resume_kind(), + JSOp::Resume => visitor.resume(), + JSOp::JumpTarget => { + let ic_index = self.next_uint32().unwrap(); + visitor.jump_target(ic_index); + } + JSOp::LoopHead => { + let ic_index = self.next_uint32().unwrap(); + let depth_hint = self.next_uint8().unwrap(); + visitor.loop_head(ic_index, depth_hint); + } + JSOp::Goto => { + let offset = self.next_int32().unwrap(); + visitor.goto_(offset); + } + JSOp::JumpIfFalse => { + let forward_offset = self.next_int32().unwrap(); + visitor.jump_if_false(forward_offset); + } + JSOp::JumpIfTrue => { + let offset = self.next_int32().unwrap(); + visitor.jump_if_true(offset); + } + JSOp::And => { + let forward_offset = self.next_int32().unwrap(); + visitor.and_(forward_offset); + } + JSOp::Or => { + let forward_offset = self.next_int32().unwrap(); + visitor.or_(forward_offset); + } + JSOp::Coalesce => { + let forward_offset = self.next_int32().unwrap(); + visitor.coalesce(forward_offset); + } + JSOp::Case => { + let forward_offset = self.next_int32().unwrap(); + visitor.case_(forward_offset); + } + JSOp::Default => { + let forward_offset = self.next_int32().unwrap(); + visitor.default_(forward_offset); + } + JSOp::TableSwitch => { + let default_offset = self.next_int32().unwrap(); + let low = self.next_int32().unwrap(); + let high = self.next_int32().unwrap(); + let first_resume_index = usize::try_from(self.next_uint24().unwrap()).unwrap(); + let count = usize::try_from(i64::from(high) - i64::from(low) + 1).unwrap(); + let offsets = + &self.resume_offsets[first_resume_index..first_resume_index + count]; + visitor.table_switch(default_offset, low, high, offsets); + } + JSOp::Return => visitor.return_(), + JSOp::GetRval => visitor.get_rval(), + JSOp::SetRval => visitor.set_rval(), + JSOp::RetRval => visitor.ret_rval(), + JSOp::CheckReturn => visitor.check_return(), + JSOp::Throw => visitor.throw_(), + JSOp::ThrowWithStack => visitor.throw_with_stack(), + JSOp::CreateSuppressedError => visitor.create_suppressed_error(), + JSOp::ThrowMsg => { + let msg_number = self.next_uint8().unwrap(); + visitor.throw_msg(msg_number); + } + JSOp::ThrowSetConst => { + let name_index = self.next_uint32().unwrap(); + visitor.throw_set_const(name_index); + } + JSOp::Try => visitor.try_(), + JSOp::TryDestructuring => visitor.try_destructuring(), + JSOp::Exception => visitor.exception(), + JSOp::ExceptionAndStack => visitor.exception_and_stack(), + JSOp::Finally => visitor.finally(), + JSOp::Uninitialized => visitor.uninitialized(), + JSOp::InitLexical => { + let localno = self.next_uint24().unwrap(); + visitor.init_lexical(localno); + } + JSOp::InitGLexical => { + let name_index = self.next_uint32().unwrap(); + visitor.init_g_lexical(name_index); + } + JSOp::InitAliasedLexical => { + let hops = self.next_uint16().unwrap(); + let slot = self.next_uint24().unwrap(); + visitor.init_aliased_lexical(hops, slot); + } + JSOp::CheckLexical => { + let localno = self.next_uint24().unwrap(); + visitor.check_lexical(localno); + } + JSOp::CheckAliasedLexical => { + let hops = self.next_uint16().unwrap(); + let slot = self.next_uint24().unwrap(); + visitor.check_aliased_lexical(hops, slot); + } + JSOp::CheckThis => visitor.check_this(), + JSOp::BindUnqualifiedGName => { + let name_index = self.next_uint32().unwrap(); + visitor.bind_unqualified_g_name(name_index); + } + JSOp::BindUnqualifiedName => { + let name_index = self.next_uint32().unwrap(); + visitor.bind_unqualified_name(name_index); + } + JSOp::BindName => { + let name_index = self.next_uint32().unwrap(); + visitor.bind_name(name_index); + } + JSOp::GetName => { + let name_index = self.next_uint32().unwrap(); + visitor.get_name(name_index); + } + JSOp::GetGName => { + let name_index = self.next_uint32().unwrap(); + visitor.get_g_name(name_index); + } + JSOp::GetArg => { + let argno = self.next_uint16().unwrap(); + visitor.get_arg(argno); + } + JSOp::GetFrameArg => { + let argno = self.next_uint16().unwrap(); + visitor.get_frame_arg(argno); + } + JSOp::GetLocal => { + let localno = self.next_uint24().unwrap(); + visitor.get_local(localno); + } + JSOp::ArgumentsLength => visitor.arguments_length(), + JSOp::GetActualArg => visitor.get_actual_arg(), + JSOp::GetAliasedVar => { + let hops = self.next_uint16().unwrap(); + let slot = self.next_uint24().unwrap(); + visitor.get_aliased_var(hops, slot); + } + JSOp::GetAliasedDebugVar => { + let hops = self.next_uint16().unwrap(); + let slot = self.next_uint24().unwrap(); + visitor.get_aliased_debug_var(hops, slot); + } + JSOp::GetImport => { + let name_index = self.next_uint32().unwrap(); + visitor.get_import(name_index); + } + JSOp::GetBoundName => { + let name_index = self.next_uint32().unwrap(); + visitor.get_bound_name(name_index); + } + JSOp::GetIntrinsic => { + let name_index = self.next_uint32().unwrap(); + visitor.get_intrinsic(name_index); + } + JSOp::Callee => visitor.callee(), + JSOp::EnvCallee => { + let num_hops = self.next_uint16().unwrap(); + visitor.env_callee(num_hops); + } + JSOp::SetName => { + let name_index = self.next_uint32().unwrap(); + visitor.set_name(name_index); + } + JSOp::StrictSetName => { + let name_index = self.next_uint32().unwrap(); + visitor.strict_set_name(name_index); + } + JSOp::SetGName => { + let name_index = self.next_uint32().unwrap(); + visitor.set_g_name(name_index); + } + JSOp::StrictSetGName => { + let name_index = self.next_uint32().unwrap(); + visitor.strict_set_g_name(name_index); + } + JSOp::SetArg => { + let argno = self.next_uint16().unwrap(); + visitor.set_arg(argno); + } + JSOp::SetLocal => { + let localno = self.next_uint24().unwrap(); + visitor.set_local(localno); + } + JSOp::SetAliasedVar => { + let hops = self.next_uint16().unwrap(); + let slot = self.next_uint24().unwrap(); + visitor.set_aliased_var(hops, slot); + } + JSOp::SetIntrinsic => { + let name_index = self.next_uint32().unwrap(); + visitor.set_intrinsic(name_index); + } + JSOp::PushLexicalEnv => { + let lexical_scope_index = self.next_uint32().unwrap(); + visitor.push_lexical_env(lexical_scope_index); + } + JSOp::PopLexicalEnv => visitor.pop_lexical_env(), + JSOp::DebugLeaveLexicalEnv => visitor.debug_leave_lexical_env(), + JSOp::RecreateLexicalEnv => { + let lexical_scope_index = self.next_uint32().unwrap(); + visitor.recreate_lexical_env(lexical_scope_index); + } + JSOp::FreshenLexicalEnv => { + let lexical_scope_index = self.next_uint32().unwrap(); + visitor.freshen_lexical_env(lexical_scope_index); + } + JSOp::PushClassBodyEnv => { + let lexical_scope_index = self.next_uint32().unwrap(); + visitor.push_class_body_env(lexical_scope_index); + } + JSOp::PushVarEnv => { + let scope_index = self.next_uint32().unwrap(); + visitor.push_var_env(scope_index); + } + JSOp::EnterWith => { + let static_with_index = self.next_uint32().unwrap(); + visitor.enter_with(static_with_index); + } + JSOp::LeaveWith => visitor.leave_with(), + JSOp::AddDisposable => { + let hint = self.next_uint8().unwrap(); + visitor.add_disposable(hint); + } + JSOp::TakeDisposeCapability => visitor.take_dispose_capability(), + JSOp::BindVar => visitor.bind_var(), + JSOp::GlobalOrEvalDeclInstantiation => { + let last_fun = self.next_uint32().unwrap(); + visitor.global_or_eval_decl_instantiation(last_fun); + } + JSOp::DelName => { + let name_index = self.next_uint32().unwrap(); + visitor.del_name(name_index); + } + JSOp::Arguments => visitor.arguments(), + JSOp::Rest => visitor.rest(), + JSOp::FunctionThis => visitor.function_this(), + JSOp::Pop => visitor.pop(), + JSOp::PopN => { + let n = self.next_uint16().unwrap(); + visitor.pop_n(n); + } + JSOp::Dup => visitor.dup(), + JSOp::Dup2 => visitor.dup2(), + JSOp::DupAt => { + let n = self.next_uint24().unwrap(); + visitor.dup_at(n); + } + JSOp::Swap => visitor.swap(), + JSOp::Pick => { + let n = self.next_uint8().unwrap(); + visitor.pick(n); + } + JSOp::Unpick => { + let n = self.next_uint8().unwrap(); + visitor.unpick(n); + } + JSOp::Nop => visitor.nop(), + JSOp::Lineno => { + let lineno = self.next_uint32().unwrap(); + visitor.lineno(lineno); + } + JSOp::NopDestructuring => visitor.nop_destructuring(), + JSOp::ForceInterpreter => visitor.force_interpreter(), + JSOp::DebugCheckSelfHosted => visitor.debug_check_self_hosted(), + JSOp::Debugger => visitor.debugger(), + } + let consumed = u32::try_from(before - self.data.len()).unwrap(); + assert_eq!(consumed, op.len(), "bytecode length mismatch for {op:?}"); + pc += consumed; + + while let Some(note) = self.try_note_at_pc() { + visitor.try_note(Pc::new(pc), ¬e); + } + } + visitor + } +} + +pub trait OpcodeVisitor { + fn before_op(&mut self, _pc: Pc, _op: JSOp, _nuses: usize, _ndefs: usize) {} + fn try_note(&mut self, _pc: Pc, _note: &TryNote) {} + fn undefined(&mut self) {} + fn null(&mut self) {} + fn false_(&mut self) {} + fn true_(&mut self) {} + fn int32(&mut self, _value: u32) {} + fn zero(&mut self) {} + fn one(&mut self) {} + fn int8(&mut self, _value: u8) {} + fn uint16(&mut self, _value: u16) {} + fn uint24(&mut self, _value: u32) {} + fn double(&mut self, _value: u64) {} + fn bigint(&mut self, _bigint_index: u32) {} + fn string(&mut self, _atom_index: u32) {} + fn symbol(&mut self, _code: u8) {} + fn void(&mut self) {} + fn typeof_(&mut self) {} + fn typeof_expr(&mut self) {} + fn typeof_eq(&mut self, _operand: u8) {} + fn pos(&mut self) {} + fn neg(&mut self) {} + fn bit_not(&mut self) {} + fn not_(&mut self) {} + fn bit_or(&mut self) {} + fn bit_xor(&mut self) {} + fn bit_and(&mut self) {} + fn eq(&mut self) {} + fn ne(&mut self) {} + fn strict_eq(&mut self) {} + fn strict_ne(&mut self) {} + fn strict_constant_eq(&mut self, _operand: u16) {} + fn strict_constant_ne(&mut self, _operand: u16) {} + fn lt(&mut self) {} + fn gt(&mut self) {} + fn le(&mut self) {} + fn ge(&mut self) {} + fn instanceof(&mut self) {} + fn in_(&mut self) {} + fn lsh(&mut self) {} + fn rsh(&mut self) {} + fn ursh(&mut self) {} + fn add(&mut self) {} + fn sub(&mut self) {} + fn inc(&mut self) {} + fn dec(&mut self) {} + fn mul(&mut self) {} + fn div(&mut self) {} + fn mod_(&mut self) {} + fn pow(&mut self) {} + fn nop_is_assign_op(&mut self) {} + fn to_property_key(&mut self) {} + fn to_numeric(&mut self) {} + fn to_string(&mut self) {} + fn is_null_or_undefined(&mut self) {} + fn global_this(&mut self) {} + fn non_syntactic_global_this(&mut self) {} + fn new_target(&mut self) {} + fn dynamic_import(&mut self) {} + fn import_meta(&mut self) {} + fn new_init(&mut self, _property_count: u8) {} + fn new_object(&mut self, _shape_index: u32) {} + fn object(&mut self, _object_index: u32) {} + fn obj_with_proto(&mut self) {} + fn init_prop(&mut self, _name_index: u32) {} + fn init_hidden_prop(&mut self, _name_index: u32) {} + fn init_locked_prop(&mut self, _name_index: u32) {} + fn init_elem(&mut self) {} + fn init_hidden_elem(&mut self) {} + fn init_locked_elem(&mut self) {} + fn init_prop_getter(&mut self, _name_index: u32) {} + fn init_hidden_prop_getter(&mut self, _name_index: u32) {} + fn init_elem_getter(&mut self) {} + fn init_hidden_elem_getter(&mut self) {} + fn init_prop_setter(&mut self, _name_index: u32) {} + fn init_hidden_prop_setter(&mut self, _name_index: u32) {} + fn init_elem_setter(&mut self) {} + fn init_hidden_elem_setter(&mut self) {} + fn get_prop(&mut self, _name_index: u32) {} + fn get_elem(&mut self) {} + fn set_prop(&mut self, _name_index: u32) {} + fn strict_set_prop(&mut self, _name_index: u32) {} + fn set_elem(&mut self) {} + fn strict_set_elem(&mut self) {} + fn del_prop(&mut self, _name_index: u32) {} + fn strict_del_prop(&mut self, _name_index: u32) {} + fn del_elem(&mut self) {} + fn strict_del_elem(&mut self) {} + fn has_own(&mut self) {} + fn check_private_field(&mut self, _throw_condition: u8, _msg_kind: u8) {} + fn new_private_name(&mut self, _name_index: u32) {} + fn super_base(&mut self) {} + fn get_prop_super(&mut self, _name_index: u32) {} + fn get_elem_super(&mut self) {} + fn set_prop_super(&mut self, _name_index: u32) {} + fn strict_set_prop_super(&mut self, _name_index: u32) {} + fn set_elem_super(&mut self) {} + fn strict_set_elem_super(&mut self) {} + fn iter(&mut self) {} + fn more_iter(&mut self) {} + fn is_no_iter(&mut self) {} + fn end_iter(&mut self) {} + fn close_iter(&mut self, _kind: u8) {} + fn optimize_get_iterator(&mut self) {} + fn check_is_obj(&mut self, _kind: u8) {} + fn check_obj_coercible(&mut self) {} + fn to_async_iter(&mut self) {} + fn mutate_proto(&mut self) {} + fn new_array(&mut self, _length: u32) {} + fn init_elem_array(&mut self, _index: u32) {} + fn init_elem_inc(&mut self) {} + fn hole(&mut self) {} + fn reg_exp(&mut self, _regexp_index: u32) {} + fn lambda(&mut self, _func_index: u32) {} + fn set_fun_name(&mut self, _prefix_kind: u8) {} + fn init_home_object(&mut self) {} + fn check_class_heritage(&mut self) {} + fn fun_with_proto(&mut self, _func_index: u32) {} + fn builtin_object(&mut self, _kind: u8) {} + fn call(&mut self, _argc: u16) {} + fn call_content(&mut self, _argc: u16) {} + fn call_iter(&mut self, _argc: u16) {} + fn call_content_iter(&mut self, _argc: u16) {} + fn call_ignores_rv(&mut self, _argc: u16) {} + fn spread_call(&mut self) {} + fn optimize_spread_call(&mut self) {} + fn eval(&mut self, _argc: u16) {} + fn spread_eval(&mut self) {} + fn strict_eval(&mut self, _argc: u16) {} + fn strict_spread_eval(&mut self) {} + fn implicit_this(&mut self) {} + fn call_site_obj(&mut self, _object_index: u32) {} + fn is_constructing(&mut self) {} + fn new_(&mut self, _argc: u16) {} + fn new_content(&mut self, _argc: u16) {} + fn super_call(&mut self, _argc: u16) {} + fn spread_new(&mut self) {} + fn spread_super_call(&mut self) {} + fn super_fun(&mut self) {} + fn check_this_reinit(&mut self) {} + fn generator(&mut self) {} + fn initial_yield(&mut self, _resume_index: u32) {} + fn after_yield(&mut self, _ic_index: u32) {} + fn final_yield_rval(&mut self) {} + fn yield_(&mut self, _resume_index: u32) {} + fn is_gen_closing(&mut self) {} + fn async_await(&mut self) {} + fn async_resolve(&mut self) {} + fn async_reject(&mut self) {} + fn await_(&mut self, _resume_index: u32) {} + fn can_skip_await(&mut self) {} + fn maybe_extract_await_value(&mut self) {} + fn resume_kind(&mut self, _resume_kind: u8) {} + fn check_resume_kind(&mut self) {} + fn resume(&mut self) {} + fn jump_target(&mut self, _ic_index: u32) {} + fn loop_head(&mut self, _ic_index: u32, _depth_hint: u8) {} + fn goto_(&mut self, _offset: i32) {} + fn jump_if_false(&mut self, _forward_offset: i32) {} + fn jump_if_true(&mut self, _offset: i32) {} + fn and_(&mut self, _forward_offset: i32) {} + fn or_(&mut self, _forward_offset: i32) {} + fn coalesce(&mut self, _forward_offset: i32) {} + fn case_(&mut self, _forward_offset: i32) {} + fn default_(&mut self, _forward_offset: i32) {} + fn table_switch(&mut self, _default_offset: i32, _low: i32, _high: i32, _offsets: &[Pc]) {} + fn return_(&mut self) {} + fn get_rval(&mut self) {} + fn set_rval(&mut self) {} + fn ret_rval(&mut self) {} + fn check_return(&mut self) {} + fn throw_(&mut self) {} + fn throw_with_stack(&mut self) {} + fn create_suppressed_error(&mut self) {} + fn throw_msg(&mut self, _msg_number: u8) {} + fn throw_set_const(&mut self, _name_index: u32) {} + fn try_(&mut self) {} + fn try_destructuring(&mut self) {} + fn exception(&mut self) {} + fn exception_and_stack(&mut self) {} + fn finally(&mut self) {} + fn uninitialized(&mut self) {} + fn init_lexical(&mut self, _localno: u32) {} + fn init_g_lexical(&mut self, _name_index: u32) {} + fn init_aliased_lexical(&mut self, _hops: u16, _slot: u32) {} + fn check_lexical(&mut self, _localno: u32) {} + fn check_aliased_lexical(&mut self, _hops: u16, _slot: u32) {} + fn check_this(&mut self) {} + fn bind_unqualified_g_name(&mut self, _name_index: u32) {} + fn bind_unqualified_name(&mut self, _name_index: u32) {} + fn bind_name(&mut self, _name_index: u32) {} + fn get_name(&mut self, _name_index: u32) {} + fn get_g_name(&mut self, _name_index: u32) {} + fn get_arg(&mut self, _argno: u16) {} + fn get_frame_arg(&mut self, _argno: u16) {} + fn get_local(&mut self, _localno: u32) {} + fn arguments_length(&mut self) {} + fn get_actual_arg(&mut self) {} + fn get_aliased_var(&mut self, _hops: u16, _slot: u32) {} + fn get_aliased_debug_var(&mut self, _hops: u16, _slot: u32) {} + fn get_import(&mut self, _name_index: u32) {} + fn get_bound_name(&mut self, _name_index: u32) {} + fn get_intrinsic(&mut self, _name_index: u32) {} + fn callee(&mut self) {} + fn env_callee(&mut self, _num_hops: u16) {} + fn set_name(&mut self, _name_index: u32) {} + fn strict_set_name(&mut self, _name_index: u32) {} + fn set_g_name(&mut self, _name_index: u32) {} + fn strict_set_g_name(&mut self, _name_index: u32) {} + fn set_arg(&mut self, _argno: u16) {} + fn set_local(&mut self, _localno: u32) {} + fn set_aliased_var(&mut self, _hops: u16, _slot: u32) {} + fn set_intrinsic(&mut self, _name_index: u32) {} + fn push_lexical_env(&mut self, _lexical_scope_index: u32) {} + fn pop_lexical_env(&mut self) {} + fn debug_leave_lexical_env(&mut self) {} + fn recreate_lexical_env(&mut self, _lexical_scope_index: u32) {} + fn freshen_lexical_env(&mut self, _lexical_scope_index: u32) {} + fn push_class_body_env(&mut self, _lexical_scope_index: u32) {} + fn push_var_env(&mut self, _scope_index: u32) {} + fn enter_with(&mut self, _static_with_index: u32) {} + fn leave_with(&mut self) {} + fn add_disposable(&mut self, _hint: u8) {} + fn take_dispose_capability(&mut self) {} + fn bind_var(&mut self) {} + fn global_or_eval_decl_instantiation(&mut self, _last_fun: u32) {} + fn del_name(&mut self, _name_index: u32) {} + fn arguments(&mut self) {} + fn rest(&mut self) {} + fn function_this(&mut self) {} + fn pop(&mut self) {} + fn pop_n(&mut self, _n: u16) {} + fn dup(&mut self) {} + fn dup2(&mut self) {} + fn dup_at(&mut self, _n: u32) {} + fn swap(&mut self) {} + fn pick(&mut self, _n: u8) {} + fn unpick(&mut self, _n: u8) {} + fn nop(&mut self) {} + fn lineno(&mut self, _lineno: u32) {} + fn nop_destructuring(&mut self) {} + fn force_interpreter(&mut self) {} + fn debug_check_self_hosted(&mut self) {} + fn debugger(&mut self) {} +} diff --git a/js/src/night/compiler/src/constants.rs b/js/src/night/compiler/src/constants.rs new file mode 100644 index 0000000000000..4a0e5835bc743 --- /dev/null +++ b/js/src/night/compiler/src/constants.rs @@ -0,0 +1,426 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Heuristic tuning constants. +//! +//! Every constant here is a *policy* level, not a correctness or ABI +//! requirement: changing one changes how much the compiler speculates, how +//! long it is willing to spend, or how large an output it will accept, and +//! never whether the result is right. They live together so the whole +//! speculation budget can be read in one place instead of being discovered +//! one `const` at a time. +//! +//! Structural limits that mirror an engine layout, a wire format or a wasm +//! spec limit are not here -- those belong next to the code that encodes +//! them, because a different value there is a bug, not a tuning choice. + +// --- analysis: contexts and callee sets ---------------------------------- + +/// Context-chain depth cap. +pub(crate) const CTX_DEPTH_CAP: u8 = 8; + +/// Distinct callees at a site before the bind degrades to CTX0. +pub(crate) const CALLEE_CAP: usize = 4; +/// Members a class region may have and still get a field view (a write +/// through an `AnyOf` receiver lands in the region view, linked into every +/// member's class view). Linking is O(members) once per (region, name); +/// past this the region is treated as megamorphic and the write dropped. +/// The callee cap is the wrong bound here: real class hierarchies commonly +/// have 20+ members, well past a call site's typical target count. +pub(crate) const REGION_VIEW_CAP: usize = 64; + +/// Total context instantiations. Raising it to 1M produces bit-identical +/// facts on the corpus while costing tens of seconds on a large bundle, so +/// this level is the compile-time gate and nothing is lost to it. +pub(crate) const CTX_BUDGET: u64 = 50_000; + +/// Cap on the per-site callee set the translator consumes: beyond this the +/// guard chain costs more than the generic dispatch saves. Independent of +/// `CALLEE_CAP`, which bounds context binding rather than emitted facts. +pub const MAX_SITE_TARGETS: usize = 4; + +/// Cap on collected fn-table members (drops are censused). +pub(crate) const TABLE_MEMBER_CAP: usize = 2048; + +// --- analysis: heap walks ------------------------------------------------ + +/// Max proto-chain hops walked by `chain_join`. +pub(crate) const CHAIN_DEPTH: usize = 8; + +/// Formals the analysis carries a per-argument cell for. Past this a call +/// binds nothing and the callee's formal reads stay unresolved. +/// +/// Note the two numberings this sits between: the analysis counts formals +/// from 0 (`FormalIndex`), while the fact tables put the receiver at 0 and +/// formal `n` at `n + 1` (`ArgIndex`). A loop over the fact-table row is +/// therefore `0..=MAX_TRACKED_FORMALS`, not `0..`. +pub(crate) const MAX_TRACKED_FORMALS: u32 = 8; + +/// Max primary (`Write`/`Deleg`) construction events recorded per script: +/// the slot-order evidence a layout row is expanded from. +pub(crate) const PRIMARY_EVENT_CAP: usize = 64; + +/// Max construction events of any kind per script. Only the `this.m(...)` +/// channel can reach it, since it does not count as a primary and so +/// nothing else would stop it. +pub(crate) const TOTAL_EVENT_CAP: usize = 128; + +/// Max method homes considered when attributing a script to a class. +pub(crate) const MAX_HOMES: usize = 8; + +/// Max distinct receiver class labels a property site accumulates before +/// it stops being evidence for anything (the region rung's input). +pub(crate) const RECV_LABEL_CAP: usize = 8; + +/// Max predicted fixed-slot fields per class layout row. +pub(crate) const LAY_CAP: usize = 16; + +/// Max constructor-delegation hops followed when expanding a layout row +/// (`Base.call(this, ...)` chains, `this.init(...)` splices). +pub(crate) const MAX_DELEG_DEPTH: u32 = 8; + +/// Depth of the index-of / element-chain def walk. +pub(crate) const IOF_WALK_DEPTH: u32 = 8; + +// --- translation: what the compiler will take on ------------------------- + +/// Size gate on a script the translator will attempt at all, in bytecode +/// terms: past it, the wasm function-size cap is the likelier outcome than a +/// compiled body, so the script stays interpreted. +pub(crate) const MAX_TRANSLATE_BYTECODE: usize = 128 * 1024; + +/// Size gate on the body a call cell may be emitted into. +pub(crate) const CALL_CELL_SCRIPT_MAX_BYTECODE: usize = 16 * 1024; + +/// Hard cap on emitted SSA values: a refined pass that overruns this +/// descends the overflow ladder (fanout-off, then GEN-only); the workqueue +/// drain aborts early once past it. Structural version identity bounds the +/// version count but not the emitted size, and the wasm function-size limit +/// and relooper tail duplication are real and independent of it. +// +/// Sized to clear real hot bodies that expand to a few hundred thousand +/// values on the full rung while still catching pathological bodies an +/// order of magnitude larger. +pub(crate) const MAX_BODY_VALUES: usize = 400_000; + +// --- translation: the versioning fixpoint -------------------------------- + +/// How many times the `Code` pass may report the map not closed before the +/// whole map is stripped. See the closure check in `translate_script`. +pub(crate) const CLOSURE_MAX_TRIES: u32 = 3; + +/// Rounds the `ContextOnly` fixpoint may take before the still-moving +/// versions are widened straight to the empty ctx. Convergence is guaranteed +/// without this (the lattice is finite and joins only descend), so the cap +/// is a compile-time bound, not a correctness one -- and widening to empty +/// is the safe direction: every lineage implies it. +/// +/// The value is calibrated with headroom over the worst script measured in +/// the corpus (the interval fixpoint piggybacks on full rounds, so each +/// widening-rung raise can cost one), and it is only the floor of a cap that +/// scales with the version population (see the fixpoint loop): a flat cap +/// stripped scripts whose lost facts were the whole point of the analysis. +/// Stripping must leave the map closed, since the `Code` pass walks against +/// every ctx the fixpoint emitted under; an unclosed strip must never pass +/// silently. +pub(crate) const CTXONLY_MAX_ROUNDS: u32 = 48; + +/// Rounds the stripped fixpoint may take to close after `strip_all`. Two is +/// the argued bound (the first stripped walk discovers the versions widening +/// brought in, the second confirms nothing moved); this leaves slack and is +/// a backstop, not a policy. +pub(crate) const STRIP_MAX_ROUNDS: u32 = 4; + +// --- translation: inlining ----------------------------------------------- + +/// Callee size cap for a polymorphic (guard-chain) inline arm. +pub(crate) const MAX_INLINE_POLY_BYTES: usize = 500; + +/// Targets a polymorphic site may splice before it stays a generic call. +pub(crate) const MAX_INLINE_TARGETS: usize = 4; + +/// Per-caller splice cap. This is an icache tuning: past a small number of +/// inlined call sites per caller the added footprint raises the icache miss +/// rate faster than it removes call overhead; below the cap the trade +/// reverses. +pub(crate) const MAX_INLINE_SITES: u32 = 8; + +/// Cap on the closure a construct admission may drag in -- what the splice +/// transitively pulls in, not just the callee's own size. +/// +/// Constructs get their own, much smaller budget because size does not +/// separate the winning splices from the losing ones. Benefit does, and a +/// ctor splice earns exactly one thing -- the field-init stores running in +/// the caller against a provably fresh `this` -- so its payoff is small and +/// Fixed however large its closure. A call splice's payoff scales with what +/// it removes, so it keeps the generous per-target caps. +/// +/// The level is one CALL_COST: a ctor with a real call out prices above it, +/// a plain field-init ctor below. Pricing the site by what it emits is what +/// this buys over a syntactic "the ctor contains a call" test -- a proven +/// apply-forward is one helper call with no classify diamond, so it stays +/// cheap and its ctor stays eligible. +pub(crate) const CONSTRUCT_CLOSURE_CAP: usize = 300; + +/// Inline arm only for small array literals: giant data-table literals +/// (thousands of InitElemArray ops) would inflate compile time for one-shot +/// init code; past the cap the generic helper is fine. +pub(crate) const INLINE_INIT_ELEM_CAP: u32 = 16; + +// --- regex --------------------------------------------------------------- + +/// Backtracks before giving up and deferring to the interpreter. +pub(crate) const BT_BUDGET: u32 = 1 << 27; + +/// Translation caps: oversized/pathological programs stay interpreted. +pub(crate) const MAX_BYTECODE_LEN: usize = 1 << 17; +pub(crate) const MAX_BT_LABELS: usize = 8192; + +// --- diagnostics --------------------------------------------------------- + +/// Per-block instruction detail cap in the `--lower` view: a data-table +/// literal can expand to thousands of stores and the view only needs shape. +pub(crate) const VIZ_BLOCK_INST_CAP: usize = 48; + +// --- guard-arm census kinds ---------------------------------------------- + +/// Census kinds for `Instrumentation::guards`: one per arm of each +/// speculation point, so a run's counts give the per-site hit rate of every +/// guard the emitter armed. Disjoint from the track-census kinds (1/2/3, +/// 47, 48/50) so both instruments can run in the same build. +/// +/// The property ladder's kinds mirror DESIGN.md section 5.2: `L1*` are the +/// class-fact arms (the analysis's own prediction), `IC_*` the per-site +/// inline cache below them. A site's total executions are the sum over its +/// kinds, and "the prediction held" is the L1 share of that. +pub(crate) mod census { + /// L1a: checkless immediate -- an upstream guard already proved it. + pub(crate) const GET_L1A: u32 = 100; + /// L1b: the folded SHALLOW|SLOTS(|RANGES) stamp test. + pub(crate) const GET_L1B_HIT: u32 = 101; + pub(crate) const GET_L1B_MISS: u32 = 102; + /// L1c: the bare SLOTS bit test under a live identity fact. + pub(crate) const GET_L1C_HIT: u32 = 103; + pub(crate) const GET_L1C_MISS: u32 = 104; + /// L1d: fused identity + SLOTS, the site-row arm with no live fact. + pub(crate) const GET_L1D_HIT: u32 = 105; + pub(crate) const GET_L1D_MISS: u32 = 106; + /// L4/W0: the IC's monomorphic way, pre-decoded fixed-slot offset. + pub(crate) const GET_IC_W0: u32 = 110; + /// L4/W1: the IC's holder tail (proto holder or dynamic slot). + pub(crate) const GET_IC_W1: u32 = 111; + /// L4/W2: both inline ways missed, entering the poly/mega probe. + pub(crate) const GET_IC_PROBE: u32 = 112; + /// L4/W3: the probe missed too -- the full miss helper. + pub(crate) const GET_IC_MISS: u32 = 113; + /// The probe's receiver: 114 keyed by `(pc << 16) | (shape >> 3)`, so + /// the count of distinct ids under one pc is the number of shapes the + /// site sees; 115 keyed by the shape's immutable-flags word (which + /// carries numFixedSlots and the slot span), site-blind. + pub(crate) const GET_IC_PROBE_SHAPE: u32 = 114; + pub(crate) const GET_IC_PROBE_SHAPE_FLAGS: u32 = 115; + + pub(crate) const SET_L1A: u32 = 120; + pub(crate) const SET_L1_HIT: u32 = 121; + pub(crate) const SET_L1_MISS: u32 = 122; + pub(crate) const SET_IC_W0: u32 = 130; + pub(crate) const SET_IC_MEGA: u32 = 131; + pub(crate) const SET_IC_TRANS: u32 = 132; + /// The add-transition replay whose proto validation the carried proof + /// discharged (a subset of `SET_IC_TRANS`). + pub(crate) const SET_IC_TRANS_PROVEN: u32 = 123; + /// `GetGName` guarded-binding arms, by site: the per-binding value + /// fuse served the read; the guarded slot load; the resolve leaf; the + /// generic helper. + pub(crate) const GNAME_FUSE_HIT: u32 = 181; + pub(crate) const GNAME_SLOT_HIT: u32 = 182; + pub(crate) const GNAME_RESOLVE: u32 = 183; + pub(crate) const GNAME_HELPER: u32 = 184; + pub(crate) const SET_IC_MISS: u32 = 133; + + /// The arithmetic guards: the typed fall-through against the generic + /// helper arm that `bbv dirties` names as box2d's second entrance. + pub(crate) const ARITH_FAST: u32 = 140; + pub(crate) const ARITH_SLOW: u32 = 141; + + /// Opt -> Dirty transition actually EXECUTED, plus the op family that + /// owns it: +0 property read, +1 property write, +2 arithmetic, + /// +3 scripted call/new, +4 everything else. The dynamic twin of the + /// `bbv dirties` static histogram. + pub(crate) const DIRTY_ENTER: u32 = 150; + + /// Same event at an indirect (unresolved-callee) call, which never + /// reaches `note_call_eff` and so was invisible to `DIRTY_ENTER`. + /// A separate kind keeps the historical direct-call numbers comparable. + pub(crate) const DIRTY_ENTER_IND: u32 = 155; + /// A side arm's track step from Opt: the fall-off event NO other + /// census could see, because it is not a call and not a guard miss -- + /// a typed-load ladder's other-type arm (a pure tag route) steps the + /// lineage down purely for version identity, and since the Side->Dirty + /// fold that step is a full track deopt. Participates in root + /// attribution like kinds 150-155. + pub(crate) const DIRTY_ENTER_SIDE_ARM: u32 = 156; + /// A builtin arm's success exit joining the call op's generic merge + /// (no keep state armed): the inline arm ran helper-free, and the + /// lineage still drops to the post-call Dirty continuation. + pub(crate) const DIRTY_ENTER_BUILTIN_MERGE: u32 = 158; + + /// Downstream-attribution bracket: PUSH right before a may-run-user-code + /// call, POP right after it returns (the pop is in the caller, so it + /// runs on the error path too -- wasm calls always return). The runtime + /// keeps a stack of "most recent departure site" cells: a departure + /// tick sets the top cell, a Dirty/Side version-entry tick attributes + /// to it, and the bracket keeps a callee's internal departures from + /// leaking into the caller's attribution. The runtime SYNTHESIZES kinds + /// 5/6 from this: downstream Dirty/Side version entries per departure + /// site -- the measured form of "recovering a departure pays in + /// proportion to executed code downstream of it". + pub(crate) const FRAME_PUSH: u32 = 60; + pub(crate) const FRAME_POP: u32 = 61; + /// `GetGName` served by a carried binding value fact (`Ctx::gcells`): + /// the fuse/slot diamond ran with no tag ladder behind it. + pub(crate) const GNAME_FACT_HIT: u32 = 62; + /// A keep continuation re-proved its carried binding facts (the + /// callee's word said a binding was written, or a helper ran). + pub(crate) const GCELL_RECHECK: u32 = 63; + + /// A compiled inline arm demoted an existing object's stamp (claim-bit + /// clear that found the bit set). The runtime advances the stamp epoch + /// on receipt, mirroring the C++ chokes' unconditional bumps + /// (vm/JSObject.h), so an unchanged epoch across a call bracket proves + /// no stamp-guarded fact died. The runtime SYNTHESIZES from the + /// comparison: kinds 11/12 = departures with stamps intact / broken, + /// kinds 13/14 = downstream Dirty/Side version entries whose ROOT + /// departure had stamps intact -- the population a keep-facts fork arm + /// ("heap written, no stamps invalidated") could recover. + pub(crate) const STAMP_DEMOTE: u32 = 65; + + /// Why a class-fact guard missed, read off the receiver's own class + /// word on the miss arm: +0 the receiver is not an object at all, +1 it + /// was never stamped (class idx 0), +2 the prediction named the WRONG + /// class, +3 the right class with the SLOTS bit clear, +4 a bucket that + /// should be unreachable (in range and stamped, yet the guard missed). + /// The get and set arms report into the same buckets, offset by 10. + pub(crate) const GET_MISS_WHY: u32 = 160; + pub(crate) const SET_MISS_WHY: u32 = 170; + + /// Which edge of `SetElem`'s fast diamond sent execution to the generic + /// helper: +0 receiver not an object, +1 key not an int32, +2 the + /// predicted-TA arm missed (class, bounds, or value kind), +3 the + /// receiver is a non-native object, +4 the append/hole check refused + /// (growth past capacity, bail flags, row probe or proto guard miss), + /// +5 the poly-TA probe returned false, +6 frozen elements. + pub(crate) const SETELEM_WHY: u32 = 24; + + /// The interior of `night_elem_append_check`'s refusal (the SETELEM_WHY + /// +4 bucket, split): kind = base + the helper's fail code. +1 append + /// past capacity, +2 elements bail flags set (append or hole path), +3 + /// append-row probe miss, +4 proto live-shape guard miss, +5 the index + /// is beyond the initialized length (neither append nor in-bounds), +6 + /// the in-bounds slot is not a hole. Guard-census builds only: the + /// helper returns these codes instead of 0 and the call site tests + /// `>= 8` and ticks the code before departing. + pub(crate) const SETELEM_APPEND_WHY: u32 = 31; + + /// The constructor exit stamp's outcome, per ctor script: the class-fact + /// guards' hit rate cannot exceed how often this store runs, so a guard + /// that never hits is usually a stamp that never fired. Each refusal + /// edge gets its own kind. + pub(crate) const STAMP_BASE: u32 = 180; + pub(crate) const RESTAMP_BASE: u32 = 190; + pub(crate) const STAMP_OK: u32 = 0; + pub(crate) const STAMP_NOT_OBJECT: u32 = 1; + pub(crate) const STAMP_NOT_OWNED: u32 = 2; + pub(crate) const STAMP_SHORT_SPAN: u32 = 3; + pub(crate) const STAMP_ALREADY: u32 = 4; + + /// The receiver's own address, ticked on a class-fact miss. The count of + /// DISTINCT ids is the answer: a handful means the misses come from + /// long-lived singletons, millions means they come from fresh + /// allocations that were never stamped. + pub(crate) const GET_MISS_RECV: u32 = 165; + pub(crate) const SET_MISS_RECV: u32 = 175; + /// The receiver's whole class word, ticked on the same miss: names + /// WHICH class the mispredicted receivers carry and which validity + /// bits (SLOTS/TYPES/RANGES) they have lost. + pub(crate) const GET_MISS_IDX: u32 = 166; + pub(crate) const SET_MISS_IDX: u32 = 176; + /// The class word a ctor-exit stamp found (id = the word), ticked on + /// the STAMP_OK path: which validity bits survived construction. + pub(crate) const STAMP_EXIT_WORD: u32 = 178; + + /// The construct fork's two arms. Arming a fork at more sites is worth + /// nothing unless the ctor's returned word is actually zero there, and + /// those are different numbers. + pub(crate) const CTOR_FORK_CLEAN: u32 = 148; + pub(crate) const CTOR_FORK_DIRTY: u32 = 149; + /// The construct fork's keep-facts arm: the ctor's word carries MUT + /// bits but not FLAG_STAMPS, so the caller rejoins Opt with its facts. + /// The flag fork's twin arm reports as track-census kind 49 (beside + /// kinds 48/50). + pub(crate) const CTOR_FORK_STAMP: u32 = 147; + /// The construct fork's dirty arm, plus which MUT bits of the runtime + /// word blocked the clean arm: +0 none, +1 MUT_THIS, +2 MUT_OTHER, + /// +3 both. `word` is `ct_delta | (callee_eff & MUT_OTHER)`, so a + /// MUT_THIS here can only have come from the allocation path. + /// (Keep clear of 180-194: the stamp/restamp outcome bands.) + pub(crate) const CTOR_FORK_WHY: u32 = 196; + /// The effect-flag fork's dirty arm, plus WHY the clean arm could not + /// take: +0 the callee's raw word was dirty but FOLDED clean from this + /// caller's perspective (a recoverable class the fork currently + /// forfeits), +1 MUT_THIS, +2 MUT_OTHER, +3 both, +4 the callee + /// returned an error. Keyed at the CALL's evidence pc (unlike kinds + /// 48/50, which are keyed at next_pc) so it joins the departure and + /// downstream records directly. + pub(crate) const FLAG_FORK_WHY: u32 = 142; + + /// Reliance census: what the CHOSEN fast form's facts rest on -- the + /// bytecode alone (intrinsic), a validated analysis claim, a tag test + /// the emitter invented (the shadow analysis), or both. kind = + /// RELY_BASE + family * 4 + Prov::class (0 intrinsic, 1 claim, 2 test, + /// 3 mixed). Ticked only where a fast form was actually emitted on the + /// strength of a ctx/operand fact, so a run's counts weight each site + /// by executions; the test-backed rows, ranked, ARE the analysis gaps. + pub(crate) const RELY_BASE: u32 = 66; + pub(crate) const RELY_ARITH_I32: u32 = 0; + pub(crate) const RELY_ARITH_NUM: u32 = 1; + pub(crate) const RELY_STRING: u32 = 2; + pub(crate) const RELY_CMP: u32 = 3; + pub(crate) const RELY_PROP_OBJ: u32 = 4; + pub(crate) const RELY_PROP_CLS: u32 = 5; + pub(crate) const RELY_ELEM: u32 = 6; + pub(crate) const RELY_IV_RUNG: u32 = 7; + + /// The on-ramp census, one TRY/OK pair per conform form: how often a + /// Dirty lineage REACHES a conform chain, and how often the chain lets + /// it back onto Opt. On-ramps are the only mechanism that returns + /// execution to Opt without a fresh function entry, so these take rates + /// bound how long a lineage dwells on Dirty after a call. + /// + /// 134-139 is clear of every other band; keep it that way. + /// + /// Loop header, from the shared funnel or a dirty entry edge: + pub(crate) const ONRAMP_TRY: u32 = 134; + pub(crate) const ONRAMP_OK: u32 = 135; + /// A conform into the RECOVERY TWIN (the dirty cycle's back edge, or + /// the twin's own excursion funnel): a site ticking TRY with no OK + /// every iteration names a population whose Opt header fact is + /// genuinely dead. + pub(crate) const CYC_ONRAMP_TRY: u32 = 136; + pub(crate) const CYC_ONRAMP_OK: u32 = 137; + /// The just-in-time on-ramp at a call return: the keep fork's runtime + /// proof failed, and the conform re-proves the successor's prediction + /// instead of dwelling on GEN until the next loop header. + pub(crate) const RET_ONRAMP_TRY: u32 = 138; + pub(crate) const RET_ONRAMP_OK: u32 = 139; +} + +/// Formals an apply-forward fast arm will fill from the caller's actuals. +/// The arm guards on the caller having passed exactly the callee's formal +/// count and then emits that many loads; past a handful it is a long +/// unrolled copy behind a guard that holds less and less often. +pub(crate) const APPLY_FWD_MAX_ARGS: u32 = 8; +/// Known-target arms an apply-forward site without a single target emits +/// (one patched identity compare each). +pub(crate) const APPLY_FWD_MAX_TARGETS: usize = 16; diff --git a/js/src/night/compiler/src/disasm.rs b/js/src/night/compiler/src/disasm.rs new file mode 100644 index 0000000000000..0ed0db99348c9 --- /dev/null +++ b/js/src/night/compiler/src/disasm.rs @@ -0,0 +1,214 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::bytecode::{JSOp, OpcodeVisitor}; +use crate::ids::Pc; + +/// Buffers one instruction's text so each line reaches the diagnostic +/// stream whole (`before_op` writes the opcode, the per-op arm appends its +/// operands and flushes). +#[derive(Default)] +pub struct Disassembler { + line: String, +} + +impl Disassembler { + fn flush(&mut self) { + crate::diag_line!("{}", self.line); + self.line.clear(); + } +} + +macro_rules! impl_disassembler { + ( + noargs: [$($noarg:ident),* $(,)?], + args: [$($op:ident($($arg:ident: $ty:ty),*)),* $(,)?] $(,)? + custom: $($custom:tt)* + ) => { + impl OpcodeVisitor for Disassembler { + fn before_op(&mut self, pc: Pc, op: JSOp, _nuses: usize, _ndefs: usize) { + self.line = format!("{pc:>6}: {op:?}"); + } + + $( + fn $noarg(&mut self) { + self.flush(); + } + )* + + $( + fn $op(&mut self, $($arg: $ty),*) { + use std::fmt::Write as _; + $(let _ = write!(self.line, " {}", $arg);)* + self.flush(); + } + )* + + $($custom)* + } + }; +} + +impl_disassembler! { + noargs: [ + undefined, null, false_, true_, + zero, one, void, typeof_, + typeof_expr, pos, neg, bit_not, + not_, bit_or, bit_xor, bit_and, + eq, ne, strict_eq, strict_ne, + lt, gt, le, + ge, instanceof, in_, lsh, + rsh, ursh, add, sub, + inc, dec, mul, div, + mod_, pow, nop_is_assign_op, to_property_key, + to_numeric, to_string, is_null_or_undefined, global_this, + non_syntactic_global_this, new_target, dynamic_import, import_meta, + obj_with_proto, init_elem, init_hidden_elem, init_locked_elem, + init_elem_getter, init_hidden_elem_getter, + init_elem_setter, init_hidden_elem_setter, get_elem, set_elem, + strict_set_elem, del_elem, strict_del_elem, has_own, + super_base, get_elem_super, set_elem_super, strict_set_elem_super, + iter, more_iter, is_no_iter, end_iter, + optimize_get_iterator, check_obj_coercible, to_async_iter, mutate_proto, + init_elem_inc, hole, init_home_object, check_class_heritage, + spread_call, optimize_spread_call, spread_eval, strict_spread_eval, + implicit_this, is_constructing, + spread_new, spread_super_call, super_fun, check_this_reinit, + generator, final_yield_rval, is_gen_closing, async_await, + async_resolve, async_reject, can_skip_await, maybe_extract_await_value, + check_resume_kind, resume, return_, get_rval, + set_rval, ret_rval, check_return, throw_, + throw_with_stack, create_suppressed_error, try_, try_destructuring, + exception, exception_and_stack, finally, uninitialized, + check_this, arguments_length, get_actual_arg, callee, + pop_lexical_env, debug_leave_lexical_env, leave_with, take_dispose_capability, + bind_var, arguments, rest, function_this, + pop, dup, dup2, swap, + nop, nop_destructuring, force_interpreter, debug_check_self_hosted, + debugger, + ], + args: [ + int32(value: u32), + int8(value: u8), + uint16(value: u16), + uint24(value: u32), + double(value: u64), + bigint(bigint_index: u32), + string(atom_index: u32), + symbol(code: u8), + typeof_eq(operand: u8), + strict_constant_eq(operand: u16), + strict_constant_ne(operand: u16), + new_init(property_count: u8), + new_object(shape_index: u32), + object(object_index: u32), + init_prop(name_index: u32), + init_hidden_prop(name_index: u32), + init_locked_prop(name_index: u32), + init_prop_getter(name_index: u32), + init_prop_setter(name_index: u32), + init_hidden_prop_getter(name_index: u32), + init_hidden_prop_setter(name_index: u32), + get_prop(name_index: u32), + set_prop(name_index: u32), + strict_set_prop(name_index: u32), + del_prop(name_index: u32), + strict_del_prop(name_index: u32), + check_private_field(throw_condition: u8, msg_kind: u8), + new_private_name(name_index: u32), + get_prop_super(name_index: u32), + set_prop_super(name_index: u32), + strict_set_prop_super(name_index: u32), + close_iter(kind: u8), + check_is_obj(kind: u8), + new_array(length: u32), + init_elem_array(index: u32), + reg_exp(regexp_index: u32), + lambda(func_index: u32), + set_fun_name(prefix_kind: u8), + fun_with_proto(func_index: u32), + builtin_object(kind: u8), + call(argc: u16), + call_content(argc: u16), + call_iter(argc: u16), + call_content_iter(argc: u16), + call_ignores_rv(argc: u16), + eval(argc: u16), + strict_eval(argc: u16), + call_site_obj(object_index: u32), + new_(argc: u16), + new_content(argc: u16), + super_call(argc: u16), + initial_yield(resume_index: u32), + after_yield(ic_index: u32), + yield_(resume_index: u32), + await_(resume_index: u32), + resume_kind(resume_kind: u8), + jump_target(ic_index: u32), + loop_head(ic_index: u32, depth_hint: u8), + goto_(offset: i32), + jump_if_false(forward_offset: i32), + jump_if_true(offset: i32), + and_(forward_offset: i32), + or_(forward_offset: i32), + coalesce(forward_offset: i32), + case_(forward_offset: i32), + default_(forward_offset: i32), + throw_msg(msg_number: u8), + throw_set_const(name_index: u32), + init_lexical(localno: u32), + init_g_lexical(name_index: u32), + init_aliased_lexical(hops: u16, slot: u32), + check_lexical(localno: u32), + check_aliased_lexical(hops: u16, slot: u32), + bind_unqualified_g_name(name_index: u32), + bind_unqualified_name(name_index: u32), + bind_name(name_index: u32), + get_name(name_index: u32), + get_g_name(name_index: u32), + get_arg(argno: u16), + get_frame_arg(argno: u16), + get_local(localno: u32), + get_aliased_var(hops: u16, slot: u32), + get_aliased_debug_var(hops: u16, slot: u32), + get_import(name_index: u32), + get_bound_name(name_index: u32), + get_intrinsic(name_index: u32), + env_callee(num_hops: u16), + set_name(name_index: u32), + strict_set_name(name_index: u32), + set_g_name(name_index: u32), + strict_set_g_name(name_index: u32), + set_arg(argno: u16), + set_local(localno: u32), + set_aliased_var(hops: u16, slot: u32), + set_intrinsic(name_index: u32), + push_lexical_env(lexical_scope_index: u32), + recreate_lexical_env(lexical_scope_index: u32), + freshen_lexical_env(lexical_scope_index: u32), + push_class_body_env(lexical_scope_index: u32), + push_var_env(scope_index: u32), + enter_with(static_with_index: u32), + add_disposable(hint: u8), + global_or_eval_decl_instantiation(last_fun: u32), + del_name(name_index: u32), + pop_n(n: u16), + dup_at(n: u32), + pick(n: u8), + unpick(n: u8), + lineno(lineno: u32), + ], + + custom: + + fn table_switch(&mut self, default_offset: i32, low: i32, high: i32, offsets: &[Pc]) { + use std::fmt::Write as _; + let _ = write!( + self.line, + " {} {} {} {:?}", + default_offset, low, high, offsets + ); + self.flush(); + } +} diff --git a/js/src/night/compiler/src/env_regions.rs b/js/src/night/compiler/src/env_regions.rs new file mode 100644 index 0000000000000..731c31cfa0b6e --- /dev/null +++ b/js/src/night/compiler/src/env_regions.rs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Mirror of the engine's reserved-region descriptor. Generated at build +//! time from the `NIGHT_ENV_REGIONS` X-macro in +//! `js/src/night/runtime/NightEnv.h`, which is the single source of truth +//! for the field list, its order and each word's wire kind. Both writers -- +//! the snapshot tool's `NightRegistration::regionTable` and the in-process +//! `env_desc` header -- fill `RegionWords` by name, so a field added, +//! removed or renamed in the header breaks the build here rather than +//! silently shifting a region base. + +include!(concat!(env!("OUT_DIR"), "/env_regions.rs")); diff --git a/js/src/night/compiler/src/facts.rs b/js/src/night/compiler/src/facts.rs new file mode 100644 index 0000000000000..a1d4f7e04dc48 --- /dev/null +++ b/js/src/night/compiler/src/facts.rs @@ -0,0 +1,563 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The likely-facts contract between the analysis (`likelier`) and the +//! translator: the output tables and their caps. Every fact is a +//! *prediction* that codegen re-checks at runtime; a wrong fact costs a +//! failed guard, never correctness. + +use rustc_hash::FxHashMap as HashMap; +use rustc_hash::FxHashSet as HashSet; + +use crate::ids::{ArgIndex, LayoutKey, NameId, Names, RegionRoot, ScriptId, Site, SlotIndex}; +pub use crate::opsem::ValueRange; +use crate::opsem::{Prims, TaKind}; + +/// A likely type claim about one value, as it travels from the analysis to +/// the translator. +/// +/// A claim is a *prediction*. The translator consumes every one of them +/// behind a runtime guard whose miss arm is correct for any value, so a +/// wrong claim costs a failed guard and a slower path, never correctness. +/// +/// A claim says one of three things: +/// +/// - nothing ([`Claim::NONE`]) -- no prediction was reached; +/// - the value is one of a set of primitive classes (in practice always a +/// purely numeric set: int32, double, or both); +/// - the value is an object or function, with no primitive component +/// ([`Claim::OBJECT`]). +/// +/// Boolean- and string-only claims are representable here but are neither +/// emitted nor consumed by this type. +/// +/// Primitive claims may additionally carry the *double-first* hint, which +/// is a hint about arm order rather than an extra type: the site's double +/// evidence is fractional-reachable (its cell range is Top with a real +/// double class), so a genuine double population is live at runtime -- a +/// fractional value has no int32 form and must double-tag, the i53 law's +/// contrapositive -- and the typed-load ladder should try the exact-double +/// form first even though int32 is in the set. +/// +/// The wire representation packs all of this into one `u16`, which is why +/// it is a type and not a bare integer: the object claim and the +/// double-first hint live in bits the primitive alphabet does not use, and +/// every consumer that wants the primitive set must go through +/// [`Claim::prims`] rather than reading the word. +#[derive(Clone, Copy, PartialEq, Eq, Default)] +pub struct Claim(u16); + +impl Claim { + /// No prediction. + pub const NONE: Claim = Claim(0); + + /// The value is an object or function (no primitive component). + pub const OBJECT: Claim = Claim(Self::OBJECT_BIT); + + const OBJECT_BIT: u16 = 1 << 15; + const DOUBLE_FIRST_BIT: u16 = 1 << 14; + /// Typed-array kind code (`TaKind::code`, 1..=9; 0 = none) beside an + /// object claim: the value is a fixed-length typed array of that kind. + const TA_SHIFT: u16 = 8; + const TA_MASK: u16 = 0xF << Self::TA_SHIFT; + + /// A claim that the value is one of `prims`. + pub fn of_prims(prims: Prims) -> Claim { + Claim(prims.bits()) + } + + /// The serialized word (for the fact dump and the diagnostic views). + pub const fn bits(self) -> u16 { + self.0 + } + + /// Rebuild from a serialized word. + pub const fn from_bits(bits: u16) -> Claim { + Claim(bits) + } + + pub const fn is_none(self) -> bool { + self.0 == 0 + } + + pub const fn is_object(self) -> bool { + self.0 & !Self::TA_MASK == Self::OBJECT_BIT + } + + /// The typed-array kind of an object claim, when it names one. + pub const fn ta_kind(self) -> Option { + if !self.is_object() { + return None; + } + crate::opsem::TaKind::from_code(((self.0 & Self::TA_MASK) >> Self::TA_SHIFT) as u8) + } + + /// An object claim narrowed to typed arrays of kind `k`. + pub const fn object_of_ta(k: crate::opsem::TaKind) -> Claim { + Claim(Self::OBJECT_BIT | ((k.code() as u16) << Self::TA_SHIFT)) + } + + /// The claim without its typed-array kind: what a read's own tag + /// ladder proves (the kind is proven by the element op's clasp guard). + pub const fn sans_ta(self) -> Claim { + Claim(self.0 & !Self::TA_MASK) + } + + /// The primitive classes claimed, with the flag bits stripped. Empty + /// for [`Claim::NONE`] and [`Claim::OBJECT`]. + pub const fn prims(self) -> Prims { + Prims::from_bits(self.0) + } + + /// Whether the typed-load ladder should take the exact-double form + /// first (see the type docs). + pub const fn double_first(self) -> bool { + self.0 & Self::DOUBLE_FIRST_BIT != 0 + } + + /// This claim with the double-first hint set. + pub const fn with_double_first(self) -> Claim { + Claim(self.0 | Self::DOUBLE_FIRST_BIT) + } +} + +impl std::fmt::Debug for Claim { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_none() { + return f.write_str("none"); + } + if self.is_object() { + return match self.ta_kind() { + Some(k) => write!(f, "object[{k:?}]"), + None => f.write_str("object"), + }; + } + write!(f, "{:?}", self.prims())?; + if self.double_first() { + f.write_str("+dblfirst")?; + } + Ok(()) + } +} + +/// Which of the two delegating call forms a call site spells. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum CallForm { + /// `T.call(this, a, b)`: the arguments are written out at the site. + Call, + /// `T.apply(this, args)`: the arguments arrive as one array value. + Apply, +} + +/// The predicted contents of one field position in a class's instance +/// layout. +#[derive(Clone, Debug, Default)] +pub struct ClassFieldFacts { + /// The field's name. Its position in [`ClassFacts::fields`] is the + /// predicted fixed-slot index: SpiderMonkey assigns slots in + /// property-creation order, so the order of first writes is the + /// prediction. + pub name: NameId, + /// Predicted value type. Only purely-numeric fields carry one. + /// Consumed by the shallow-conformance machinery: checked stores + /// maintain "this field holds a value of this type", the + /// shallow-conforming flag asserts it, and conforming loads then skip + /// the value tag check. + pub prims: Prims, + /// Predicted value range. A range rides its own stamp bit (RANGES) + /// rather than TYPES, because it is consumed checklessly and so cannot + /// survive the engine choke's numberness-only maintenance. Claimed + /// only at positions that also carry a `prims` claim: the range is the + /// value's magnitude and `prims` is its tag, and no consumer wants one + /// without the other. + pub range: Option, + /// The effective claim in fullword/dims mode: `prims` where the write + /// tier claimed one, otherwise the name-keyed claim filled in from the + /// typed tier. Equal to `prims` when the typed tier adds nothing. + pub typed_prims: Prims, +} + +/// The predicted instance layout of one class, keyed in +/// [`LikelyFacts::classes`] by its [`LayoutKey`] -- which is what +/// `ctor_stamps`, `this_layouts` and `deleg_restamps` point at, not the +/// constructor script. +/// +/// Consumed via guard cells that the C++ validator checks at runtime, so a +/// wrong layout costs the fast path and never correctness. +#[derive(Clone, Debug, Default)] +pub struct ClassFacts { + pub fields: Vec, +} + +/// How the analysis resolved one call site. +/// +/// The arms are mutually exclusive outcomes, not flags: a site whose every +/// evaluation agreed on one modeled native has no scripted callee to offer, +/// and a site with scripted callees never settled on a native. Making that +/// an enum rather than two parallel tables is what keeps a consumer from +/// having to ask both and decide which wins. +#[derive(Clone, Debug)] +pub enum CallResolution { + /// Every evaluation agreed on one modeled bare-name native the + /// translator has an inline arm for. *Which* native is not part of the + /// fact: the arm is selected at emission from the callee itself, so + /// this only says the site has a single modeled native behind it, and + /// the runtime callee-identity guard makes a wrong answer a missed fast + /// path rather than a miscompile. + Native, + /// `1..=MAX_SITE_TARGETS` scripted callees, for the guarded dispatch + /// and inlining arms. A singleton also arms the guarded direct call; a + /// small polymorphic set arms the guard chain. + Scripted(Vec), +} + +/// Post-fixpoint per-script effect summary: what a call to this script, +/// its resolved callees folded in transitively, may write to pre-existing +/// heap. Produced from the solved state only, never fed into the solve. +/// `top` means the walk met an op or a call edge it could not classify; +/// the other fields are meaningless then. Call resolution is likely, not +/// proven, so the summary shares the facts contract: a consumer keeps +/// only guarded/recoverable state on its strength. +#[derive(Clone, Debug, Default)] +pub struct EffectSummary { + pub top: bool, + /// What saturated the summary (an op name, "cap", a call-edge reason). + /// Diagnostic only: excluded from equality, so the summary fixpoint + /// cannot churn on why-strings propagating around a recursive cycle. + pub top_why: Option, + /// Property writes: the write-site receiver's layout-key range when + /// its receivers agreed on a planned class, else None = unknown + /// receiver. + pub field_writes: Vec<(Option<(LayoutKey, LayoutKey)>, NameId)>, + pub gname_writes: Vec, + pub elems_write: bool, + pub env_write: bool, +} + +impl PartialEq for EffectSummary { + fn eq(&self, other: &EffectSummary) -> bool { + self.top == other.top + && self.field_writes == other.field_writes + && self.gname_writes == other.gname_writes + && self.elems_write == other.elems_write + && self.env_write == other.env_write + } +} +impl Eq for EffectSummary {} + +impl EffectSummary { + pub fn saturate(&mut self, why: impl Into) { + if !self.top { + self.top = true; + self.top_why = Some(why.into()); + } + } + + pub fn is_write_free(&self) -> bool { + !self.top + && self.field_writes.is_empty() + && self.gname_writes.is_empty() + && !self.elems_write + && !self.env_write + } + + /// One-token diagnostic label: `wf`, `w:f/g//`, + /// or `top:`. + pub fn label(&self) -> String { + if self.top { + format!("top:{}", self.top_why.as_deref().unwrap_or("?")) + } else if self.is_write_free() { + "wf".to_string() + } else { + format!( + "w:{}f/{}g/{}/{}", + self.field_writes.len(), + self.gname_writes.len(), + u8::from(self.elems_write), + u8::from(self.env_write), + ) + } + } +} + +/// Everything the analysis tells the translator: the whole contract +/// between `likelier` and `wasm`, and the only channel between them. +/// +/// Every field is a *prediction*. The translator emits each one behind a +/// runtime guard with a generic fallback, so a wrong entry costs a failed +/// guard and a slower path, never a wrong answer -- which is what lets the +/// analysis be as aggressive as it likes. +/// The slot half of a `local_restamps` entry names a formal (the low bits +/// its index) when this bit is set, else a local. +pub const RESTAMP_FORMAL: u32 = 1 << 31; + +/// A builtin an apply-form site forwards to (see `LikelyFacts::apply_natives`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ApplyNative { + HasOwnProperty, +} + +#[derive(Default)] +pub struct LikelyFacts { + /// The compilation's string table, handed on to the translator: every + /// `NameId` in the tables below resolves through it, and the emitted + /// atom table is built on top of it rather than as a second copy. + pub names: Names, + /// Per-property-site resolved accessor: (target + /// getter/setter script, kind 0 = get / 1 = set). Produced from the + /// modeled `Object.defineProperty` class accessor table at sites + /// whose receivers agree on one class. Consumed by the accessor-call + /// arm (a runtime-primed (shape, atom) cache guards receiver and + /// callee identity, so a wrong likely only misses to the IC path). + pub accessor_sites: HashMap, + /// Names registered as accessors on any class: sites reading/writing + /// these names whose receivers did not classify still emit the + /// (fully dynamically guarded) accessor arm, without a static target. + pub accessor_names: HashSet, + /// How each call site resolved (see [`CallResolution`]). Absent = the + /// site did not resolve and takes the generic dispatch. + pub call_sites: HashMap, + /// Every syntactic apply/call-shaped call site (the callee node is an + /// `.apply`/`.call` property read), resolved or not, with the form it + /// spells. The apply-forward flow check keys on this -- the forward + /// helper reads the real callee/target from the stack at runtime, so + /// compile-time target resolution is not required for soundness. + pub apply_sites: HashMap, + /// The single scripted target of an apply/call-shaped site, where the + /// receiver of the `.apply`/`.call` resolved mono. The forward helper + /// does not need this -- it reads the real target off the stack -- but a + /// site that wants to call the target DIRECTLY does, and the direct call + /// is what would give an apply-forward site a truthful effect word + /// instead of the opaque helper's saturation. + pub apply_targets: HashMap, + /// `apply_targets` resolved per entry: (the call/construct site that + /// entered the apply site's body, the apply site) -> the single target + /// under that entry. A shared wrapper's `this.initialize.apply` is + /// multi at the body and mono per `new` site; a splice of the body at + /// that site reads its target here. + pub apply_targets_in: HashMap<(Site, Site), ScriptId>, + /// Every scripted target an apply/call-shaped site is known to reach: + /// the body-level set where it stayed within bound, plus every + /// per-entry resolution. Sorted. A site with no single target still + /// calls each of these directly, by callee identity. + pub apply_target_sets: HashMap>, + /// Per apply-form site: the builtin its mono target is, for the + /// codegen's native forward arms (`hasOwnProperty.call(o, k)`). + /// Guarded by callee identity at runtime, so a wrong resolution is a + /// missed fast path. + pub apply_natives: HashMap, + /// Per-script transitive effect summaries (see [`EffectSummary`]). + /// Every script the source carries has an entry. + pub script_effects: HashMap, + /// Method script -> (lo key, hi key) of the predicted `this` class: + /// lo == hi = an exact ctor-class home (the narrowed subclass view); + /// lo < hi = a predictor-class home consumed with a range guard and + /// the group table. + pub this_layouts: HashMap, + /// Predicted instance layout per class, keyed by layout key. + pub classes: HashMap, + /// Array element claims, keyed by the array class-region root (the + /// union-find root, so sibling alloc sites that flowed together share + /// one claim): root -> (element type, element range). Arrays carry the same + /// stamp word objects do; the claim covers a non-hole element's value, + /// which is all a reader ever sees -- the dense read hole-checks before + /// the value reaches any consumer. + pub array_elem_claims: HashMap, + /// Array allocation site its class-region root: where + /// a compiled allocation stamps the fresh array. + pub array_alloc_sites: HashMap, + /// Element site the predicted receiver's class-region + /// root, for reads (the fold) and writes (the maintenance duty). + pub array_elem_recv: HashMap, + /// Per-property-site likely receiver: (lo key, hi + /// key, predicted slot, mask). lo == hi = exact ctor-class fact + /// (narrowed `this`); lo < hi = range fact over the group table OR a + /// per-name sub-range of agreeing contiguous member keys. The mask is + /// computed at emission (per-ctor for exact, all-members-claim for + /// ranges) because sub-range los are not group los -- consumers must + /// not recompute it from group tables. + pub prop_sites: HashMap, + /// Per-element-read-site likely value prims: at a GetElem, + /// the receiver class's merged `[]` element node prim mask, exported + /// only when purely numeric (Int32|Double). Consumed by loop versioning + /// as a per-read value-tag-guarded assumption (a wrong likely deopts + /// that read to the generic loop copy; never correctness). + pub elem_sites: HashMap, + /// Per-element-WRITE-site: the receiver array region's merged `[]` + /// element node prim mask (the mask the region's reads claim), + /// exported only when purely numeric. A store site whose mask admits + /// Double may box an integral double as a double: every read of that + /// node already admits the tag, so the int32 canonicalisation buys + /// nothing there. + pub elem_write_sites: HashMap, + /// Per-script likely this/arg types (the guard-at-defs family: the Opt + /// track is kept aligned with the likelier's predictions by guarding at + /// defs whose produced type does not already imply the claim): + /// (script, arg index) -> claim. Bits 0-5 = a purely-numeric PRIM_* mask; + /// 0x8000 = object-only (no primitive component). Joined over live + /// analysis ctxs; P_UNKNOWN or mixed prim/object evidence emits no + /// claim. Consumed at GetArg as a one-tag-test guard whose positive + /// side continues with the fact; a def whose type already implies + /// the claim takes no guard and keeps the tighter type. + pub arg_types: HashMap<(ScriptId, ArgIndex), Claim>, + /// Per-formal VALUE class range (emitted layout-key space), the + /// advisory sibling of `arg_types`: the entry ctx carries it as a + /// `likely_cls` hint, unguarded; the first use that needs the identity + /// guards it (the lazy tier). Index convention follows `arg_types` + /// (1 + formal; `this` resolves through `this_layouts` instead). + pub arg_cls: HashMap<(ScriptId, ArgIndex), (LayoutKey, LayoutKey)>, + /// Per-call-site likely result types (guard-at-defs, the call + /// family): numeric mask or 0x8000 object-only, + /// from the call's ret cell joined over live ctxs. Object claims + /// only under receiver demand (the result feeds a property/element + /// access); consumed at the generic call continuation as a + /// one-tag-test ladder. + pub call_types: HashMap, + /// Per-GetAliasedVar-site likely value prims: the + /// resolved (scope, slot) cell's prim mask, exported under the same + /// purely-numeric gate as `elem_sites`. Env slots are written through + /// SetAliasedVar barriers by any closure sharing the scope, so the + /// fact is likely only -- consumed as a tag-guarded arm-order hint. + pub aliased_sites: HashMap, + /// Per-global-name likely value types (the guard-at-defs family + /// applied to the global store): name -> claim, projected from the + /// engine's context-free GName cells -- the snapshot global's initial + /// value joined with every statically-seen SetGName write. Numeric + /// claims are demand-free; object claims only under element-receiver + /// demand (the arg_types discipline). Likely, never proof: writes the + /// scan cannot see (eval'd scripts, computed-key global stores) make + /// the consumer's per-read tag guard miss, never a miscompile -- so + /// unlike the fused-literal machinery this table needs no fuses. + pub gname_types: HashMap, + /// Arith sites whose RESULT cell is fractional-reachable (double + /// evidence at range Top -- the i53 law's contrapositive): a real + /// runtime double population flows through the op, so its + /// both-number f64 arm keeps the Opt track (the numeric-category + /// policy). Sites absent here keep the track step: their f64 arm is + /// cold, and letting its numeric result join the successor would + /// degrade a pure-int32 chain's facts. + pub fractional_arith_sites: HashSet, + /// Arith (`+`) sites whose result cell carries string evidence: a real + /// string population flows through the op, so the both-string concat + /// arm keeps the Opt track (the string analog of the numeric-category + /// policy). Sites absent here keep the track step for the same + /// join-degradation reason as `fractional_arith_sites`. + pub string_arith_sites: HashSet, + /// Per-element-site likely typed-array kind: at a GetElem/SetElem, + /// the receiver class's element kind when it settled on a single one. Consumed as a guarded-monomorphic inline + /// read/store arm (clasp guard + kind-specific access); a wrong prediction + /// just misses to the generic helper, never a correctness issue. + pub ta_elem_sites: HashMap, + /// Elem sites that get the polymorphic TA arm (shared in-module helper): + /// every elem site in a bundle that references a typed-array constructor + /// (natives like `subarray()` hand TAs to sites the class analysis tags + /// non-TA, so per-class gating loses them); Empty for TA-free bundles, + /// where the arm's cold call would lengthen hot dense-loop live ranges. + pub elem_poly_sites: HashSet, + /// Per-read-site VALUE class range, in the emitted layout-key space: + /// the object this site loads is likely of a class in [lo, hi]. The + /// consumer attaches it as an ADVISORY `likely_cls` on the result -- + /// unchecked until a use synthesizes a class-fact row from it, whose + /// own guard proves (or misses) it lazily. + pub field_cls_sites: HashMap, + /// Per-field-read-site likely value prims: GetProp the + /// receiver class's field node prim mask, exported under the same + /// purely-numeric gate as `elem_sites` and consumed the same way (a + /// per-read value-tag-guarded assumption inside versioned loops). + pub field_sites: HashMap, + /// Diagnostic: classes discovered / constraints collected. + pub n_classes: usize, + pub n_cons: usize, + /// Scripts homed as this-forwarded delegates of a class (the + /// static-init idiom): their `this.f = v` stores are instance inits, + /// so the layout-set slow tail carries the add-transition arm there + /// (and only there -- it is pure bloat on method overwrite tails). + pub deleg_inits: HashSet, + /// Ctor-return stamp sites: constructor script -> its own ctor-class + /// key (contiguous within the predictor group). + pub ctor_stamps: HashMap, + /// Object-literal stamp sites: the `NewInit`/`NewObject` site -> its + /// lit-row layout key. The rows always existed in the key space (and + /// so in the runtime layout tables and the per-site claims); this is + /// the site mapping that lets the allocation actually STAMP, so the + /// literal-born population stops being the "receiver never stamped" + /// class-fact miss bucket. + pub lit_stamps: HashMap, + /// Construct-site allocation sizing: ctor script -> the full layout + /// row length (two-phase ctors count the delegate-assigned suffix). + /// Consumed as the `new`-site nSlots prediction so every predicted + /// field lands in a fixed slot regardless of the engine's ctor-body + /// property-count estimate. + pub ctor_nslots: HashMap, + /// Predictor-group tables, keyed by the group's LO key: (universal + /// prefix field names, per-slot masks claimed by every member). + /// Consumed by range facts (lo < hi). + pub group_tables: HashMap, Vec)>, + /// Shared-generated-ctor construct sites (the prototype.js + /// `Class.create()` idiom: many classes, one ctor script, so + /// script-keyed stamps cannot key them): the layout + /// key of the class the site's snapshot-resolved callee object + /// constructs. Derived concretely (callee def-chain -> function + /// object -> its `.prototype` -> the init delegate the ctor's + /// `this..apply` dispatch reaches there); the init delegate + /// enters `deleg_restamps`/`deleg_inits`/`this_layouts` so the row + /// stamps and its adds ride the static checks. Consumed by the + /// construct-site alloc word/size in place of `ctor_stamps`/ + /// `ctor_nslots` when those (script-keyed) miss. + pub construct_site_keys: HashMap, + /// Two-phase construction re-stamp sites: init-delegate script -> the + /// full layout key of the two-phase ctor it completes. At each return + /// of the delegate, an object `this` whose live shape equals the full + /// row's validated shape is (re-)stamped with the full key -- the + /// prefix-stamped (or cleared) word from the ctor-exit phase advances + /// to the full id, so full-only field guards start hitting. + pub deleg_restamps: HashMap, + /// Formal-receiver fill scripts: script -> (formal index, full layout + /// key). The `this`-delegate rule's sibling for the `nbi()`-then-fill + /// idiom, where a fresh prefix-stamped object is completed through an + /// ARGUMENT (crypto's `multiplyTo(a, r)` writing `r.t`/`r.s`): the + /// script's own formal-receiver writes contribute a suffix name of the + /// two-phase full row, so each return re-stamps the formal's object to + /// the full key under the same validated-shape gates. Without this the + /// population never advances and every full-key read guard misses. + pub arg_restamps: HashMap, + /// Post-construction fill sites: (script, pc of the last add) -> + /// (local index, full layout key). The local-receiver sibling of + /// `arg_restamps`: instances filled after construction by a + /// straight-line add sequence on a local (box2d's `ccp = c.points[j]`) + /// are restamped to the full key after the last add. + pub local_restamps: HashMap, + /// Name-keyed type facts (the type dimension, independent of the slot + /// dimension): (lo key, hi key, mask) for property + /// read/write sites whose receiver class(es) uniformly claim a numeric + /// value mask for the accessed name -- including names absent from + /// every layout (post-init fields) and classes whose slot prediction + /// never validates. Consumed by the types-only ladder arm (IC-served + /// load, typed push) and the store-side name-keyed conform mask. + pub typed_sites: HashMap, +} + +impl LikelyFacts { + /// The scripted callees of a site: empty when it did not resolve, or + /// resolved to a native instead. + pub fn scripted_targets(&self, site: Site) -> &[ScriptId] { + match self.call_sites.get(&site) { + Some(CallResolution::Scripted(t)) => t, + _ => &[], + } + } + + /// Whether the site resolved to a modeled native with an inline arm. + pub fn is_native_call(&self, site: Site) -> bool { + matches!(self.call_sites.get(&site), Some(CallResolution::Native)) + } + + /// Every site that resolved to scripted callees, with them. + pub fn scripted_call_sites(&self) -> impl Iterator { + self.call_sites.iter().filter_map(|(&site, r)| match r { + CallResolution::Scripted(t) => Some((site, t.as_slice())), + CallResolution::Native => None, + }) + } +} diff --git a/js/src/night/compiler/src/ids.rs b/js/src/night/compiler/src/ids.rs new file mode 100644 index 0000000000000..6fd2108982bd3 --- /dev/null +++ b/js/src/night/compiler/src/ids.rs @@ -0,0 +1,476 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Identifier newtypes for the analysis/translator contract. +//! +//! These exist so the compiler catches what review cannot: the fact tables +//! are keyed by pairs of small integers, and a swapped script/pc or a class +//! key used where a slot index belongs are mistakes a `u32` cannot report. + +use crate::source::SourceObjectId; + +/// A compiled script, named by its id in the source object graph. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct ScriptId(u32); + +impl ScriptId { + pub const fn new(id: u32) -> ScriptId { + ScriptId(id) + } + + pub const fn get(self) -> u32 { + self.0 + } + + pub const fn source(self) -> SourceObjectId { + SourceObjectId::new(self.0) + } +} + +impl std::fmt::Display for ScriptId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A bytecode offset within one script. +/// +/// Offsets carry their own type all the way through the analysis and the +/// lowering, so an offset can never be passed where a script id, an +/// argument position or a slot index belongs -- the mistakes a `u32` cannot +/// report. The arithmetic an offset genuinely needs is spelled out on the +/// type: advance by an instruction length, rebase into a spliced segment, +/// and resolve a relative branch. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct Pc(u32); + +impl Pc { + pub const fn new(pc: u32) -> Pc { + Pc(pc) + } + + pub const fn get(self) -> u32 { + self.0 + } + + /// The absolute target of a relative branch here with signed offset + /// `off` -- the interpreter's own wrapping arithmetic. + pub fn branch(self, off: i32) -> Pc { + Pc((i64::from(self.0) + i64::from(off)) as u32) + } +} + +/// Advance by an instruction length (`pc + op.len()`): the only arithmetic +/// bytecode offsets need, and it stays within one script by construction. +impl std::ops::Add for Pc { + type Output = Pc; + fn add(self, rhs: u32) -> Pc { + Pc(self.0 + rhs) + } +} + +/// Rebase into a spliced segment's local offset space (`pc - seg.base`). +impl std::ops::Sub for Pc { + type Output = Pc; + fn sub(self, rhs: u32) -> Pc { + Pc(self.0 - rhs) + } +} + +impl std::ops::AddAssign for Pc { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs; + } +} + +impl std::fmt::Display for Pc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// One bytecode operation in the whole program: the key of every per-site +/// fact table. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct Site { + pub script: ScriptId, + pub pc: Pc, +} + +impl Site { + pub const fn new(script: ScriptId, pc: Pc) -> Site { + Site { script, pc } + } + + /// From raw ids, for the FFI and dump boundaries that carry loose + /// integers. + pub const fn from_raw(script: u32, pc: u32) -> Site { + Site::new(ScriptId::new(script), Pc::new(pc)) + } +} + +impl std::fmt::Display for Site { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.script, self.pc) + } +} + +/// A predicted instance layout, named by the dense key a compiled +/// allocation stamps into an object's class word. Distinct from +/// `likelier::heap::ClassKey`, which names a class by its identity +/// (prototype object, constructor script, or allocation site) inside the +/// analysis; this is the emitted, translator-facing id. Keys of one predictor group are +/// contiguous, which is what lets a range guard cover a whole group. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct LayoutKey(u32); + +impl LayoutKey { + /// One past the last usable key. + /// + /// A stamped object carries its key in a 15-bit field of its class + /// word, so the key space is an ABI limit shared by the analysis (which + /// stops minting), the environment layout (which asserts) and the + /// lowering (which range-guards on it) -- not a tuning parameter any + /// one of them may raise alone. + pub const LIMIT: u32 = 0x7FFF; + + pub const fn new(key: u32) -> LayoutKey { + LayoutKey(key) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +impl std::fmt::Display for LayoutKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// The identity half of an object's stamp word: a [`LayoutKey`] biased by +/// one, so that 0 means "unstamped" and every real layout has a nonzero key. +/// +/// This is the number a compiled allocation writes, a ctor-exit stamp +/// carries, and every class-fact guard compares against -- and it is off by +/// one from the [`LayoutKey`] the analysis and the layout tables use. The two +/// are both small integers naming the same layout, which is exactly why they +/// carry different types: a guard emitted against an unbiased key silently +/// tests the neighbouring layout. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct StampKey(u32); + +impl StampKey { + /// The unstamped word: no compiled allocation ever writes it. + pub const NONE: StampKey = StampKey(0); + + pub const fn new(k: u32) -> StampKey { + StampKey(k) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +impl LayoutKey { + /// The stamp this layout's objects carry. + pub const fn stamp(self) -> StampKey { + StampKey(self.0 + 1) + } +} + +impl std::fmt::Display for StampKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// The position of a field within a predicted instance layout: an index +/// into the layout row, which is also the object's fixed-slot index. +/// +/// Distinct from [`LayoutKey`], which names the layout as a whole. The two +/// are both small integers and both appear in the same fact rows, which is +/// exactly why they carry different types. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct SlotIndex(u32); + +impl SlotIndex { + pub const fn new(i: u32) -> SlotIndex { + SlotIndex(i) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +impl std::fmt::Display for SlotIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// An argument position within one script as the *fact tables* number +/// them: 0 is `this`, `1 + n` is formal `n`. Deliberately not a [`Pc`]: +/// the arg tables are keyed by position, and a bare `(u32, u32)` key here +/// is indistinguishable from the per-site tables' `(script, pc)` while +/// meaning something else entirely. The analysis's own numbering is +/// [`FormalIndex`], which counts formals from 0 and keeps the receiver in +/// a cell of its own. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct ArgIndex(u32); + +impl ArgIndex { + /// The receiver. + pub const THIS: ArgIndex = ArgIndex(0); + + pub const fn new(i: u32) -> ArgIndex { + ArgIndex(i) + } + + /// Formal `n` (which is index `1 + n`). + pub const fn formal(n: u32) -> ArgIndex { + ArgIndex(1 + n) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +impl std::fmt::Display for ArgIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A formal parameter's position within one script, counted from 0 +/// (`function f(a, b)`: `a` is 0). This is the analysis's numbering, where +/// the receiver is not an argument at all but its own cell; the emitted +/// fact tables use [`ArgIndex`], which numbers the receiver 0 and formal +/// `n` as `n + 1`. Two spaces one apart is exactly the confusion a +/// shared `u32` cannot report. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct FormalIndex(u32); + +impl FormalIndex { + pub const fn new(i: u32) -> FormalIndex { + FormalIndex(i) + } + + pub const fn get(self) -> u32 { + self.0 + } + + /// The same position in the fact tables' numbering. + pub const fn as_arg_index(self) -> ArgIndex { + ArgIndex::formal(self.0) + } +} + +impl std::fmt::Display for FormalIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A slot in a closure environment (`CallObject`) -- the index an +/// aliased-variable access names within its scope. Not a [`VarId`]: the +/// scanner's variable numbering and a scope's slot numbering are different +/// spaces that share `u32`'s shape. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct EnvSlot(u32); + +impl EnvSlot { + pub const fn new(slot: u32) -> EnvSlot { + EnvSlot(slot) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +impl std::fmt::Display for EnvSlot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// The union-find root of a class region: sibling allocation sites whose +/// values flowed together share one root, and therefore one array claim. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct RegionRoot(u32); + +impl RegionRoot { + pub const fn new(r: u32) -> RegionRoot { + RegionRoot(r) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +impl std::fmt::Display for RegionRoot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A local/temporary slot in the analysis's per-script variable numbering +/// (`engine::CKey::Var`). Not a [`Pc`] and not an [`ArgIndex`]: the solver's +/// callee-tracking tables are keyed by script and variable, which is a +/// different space that happened to share `(u32, u32)`'s shape. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct VarId(u32); + +impl VarId { + pub const fn new(v: u32) -> VarId { + VarId(v) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +impl std::fmt::Display for VarId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A JavaScript string: property and binding names, string literals in the +/// source object graph, regex patterns. +/// +/// JS strings are sequences of UTF-16 code units, not Rust `str`s -- they may +/// hold unpaired surrogates, so they do not always round-trip through UTF-8, +/// which is why the whole compiler carries them as code units. Naming the +/// type is what keeps a *string* distinct from the many other `Vec` +/// buffers around it, gives the name-keyed fact tables a key type that says +/// so, and gives every diagnostic one `Display` instead of a lossy conversion +/// open-coded at each site. +/// +/// Ordering, hashing and `Borrow<[u16]>` are the underlying buffer's, so a +/// `JsString` key hashes exactly as the `Vec` it replaced. +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)] +pub struct JsString(Vec); + +impl JsString { + pub fn from_chars(chars: Vec) -> JsString { + JsString(chars) + } + + pub fn chars(&self) -> &[u16] { + &self.0 + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Whether this is the name `s`, which is always ASCII at the call sites + /// that ask (`length`, `charCodeAt`, the well-known method names). + pub fn is(&self, s: &str) -> bool { + self.0.iter().copied().eq(s.encode_utf16()) + } +} + +/// So a `HashMap` can be probed with a bare `&[u16]`. Sound +/// because the derived `Hash` and `Eq` are the code-unit slice's own. +impl std::borrow::Borrow<[u16]> for JsString { + fn borrow(&self) -> &[u16] { + &self.0 + } +} + +/// Names deref to their code units, the way `String` derefs to `str`: the +/// slice operations are the same ones, and a `&JsString` passed where a +/// `&[u16]` is wanted coerces. +impl std::ops::Deref for JsString { + type Target = [u16]; + fn deref(&self) -> &[u16] { + &self.0 + } +} + +impl From<&str> for JsString { + fn from(s: &str) -> JsString { + JsString(s.encode_utf16().collect()) + } +} + +impl std::fmt::Display for JsString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for c in char::decode_utf16(self.0.iter().copied()) { + write!(f, "{}", c.unwrap_or(char::REPLACEMENT_CHARACTER))?; + } + Ok(()) + } +} + +/// A JS string interned in the compilation's one string table. +/// +/// Every UTF-16 string the compiler names -- property names, global +/// bindings, layout field names -- gets one id here, assigned once and used +/// from the analysis through to emission. Comparing or keying by `NameId` is +/// an integer compare rather than a code-unit-buffer hash, and a name that +/// crosses the analysis/translator boundary crosses it as an id rather than +/// as a copy. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)] +pub struct NameId(pub u32); + +/// The compilation's one string table: `NameId <-> JsString`. +/// +/// Built before the analysis (the syntactic global-binding scan seeds it), +/// filled by the analysis, handed to the translator in `LikelyFacts`, and +/// finally owned by the `AtomTable`, which adds the emitted table's dense +/// numbering on top without a second copy of the strings. +#[derive(Default)] +pub struct Names { + by_val: std::collections::HashMap, + vals: Vec, +} + +impl Names { + /// Intern a Rust literal, without the caller materializing the UTF-16 + /// buffer itself. + pub fn intern_str(&mut self, s: &str) -> NameId { + let chars = JsString::from(s); + self.intern(&chars) + } + + pub fn intern(&mut self, n: &[u16]) -> NameId { + if let Some(&id) = self.by_val.get(n) { + return id; + } + let id = NameId(u32::try_from(self.vals.len()).unwrap()); + self.vals.push(JsString::from_chars(n.to_vec())); + self.by_val.insert(JsString::from_chars(n.to_vec()), id); + id + } + + pub fn get(&self, id: NameId) -> &JsString { + &self.vals[id.0 as usize] + } + + pub fn lookup(&self, n: &[u16]) -> Option { + self.by_val.get(n).copied() + } + + pub fn lossy(&self, id: NameId) -> String { + String::from_utf16_lossy(self.get(id)) + } + + pub fn len(&self) -> usize { + self.vals.len() + } + + pub fn is_empty(&self) -> bool { + self.vals.is_empty() + } +} diff --git a/js/src/night/compiler/src/lib.rs b/js/src/night/compiler/src/lib.rs new file mode 100644 index 0000000000000..4ecb8aec975c3 --- /dev/null +++ b/js/src/night/compiler/src/lib.rs @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Stylistic lints the emitter's shape legitimately conflicts with: lowering +// entry points take many arguments, `to_*` converts an operand rather than +// `self`, and rustdoc list formatting is not a goal. +#![allow( + clippy::too_many_arguments, + clippy::type_complexity, + clippy::wrong_self_convention, + clippy::large_enum_variant, + clippy::doc_lazy_continuation, + clippy::doc_overindented_list_items +)] +//! NightMonkey: an ahead-of-time compiler from SpiderMonkey bytecode to +//! WebAssembly. +//! +//! The compiler takes a snapshot of a JavaScript program -- its scripts plus +//! the object graph they have built by the end of setup -- and emits a Wasm +//! module that runs it, with no bytecode interpreter on the hot path. +//! +//! JavaScript has no static types, so a translation that committed to +//! nothing would be a threaded interpreter in Wasm clothing: every operand +//! boxed, every operator a helper call. NightMonkey instead runs a +//! whole-program *likely-types* analysis and compiles code specialized to +//! what it finds -- unboxed int32 and double values, direct calls, inline +//! property loads at predicted slots. +//! +//! What the analysis produces is a **prediction, never a proof**. Every +//! specialization is emitted behind a runtime guard, and every guard has a +//! generic fallback that is correct for any value. Correctness therefore +//! rests entirely on the guards and the fallbacks; a wrong prediction costs +//! a failed guard and a slower path, never a wrong answer. Nothing in the +//! analysis has to be sound, which is what lets it be aggressive. Scripts +//! the translator cannot handle are simply left interpreted. +//! +//! The pipeline, in order: +//! +//! - [`source`] -- the input object graph, built on the SpiderMonkey side +//! and handed over through the FFI in [`source::ffi`]. The sole input: +//! the compiler reads no other channel, and in particular takes no +//! profiling data. +//! - [`bytecode`] -- the SpiderMonkey bytecode reader ([`disasm`] dumps it). +//! - [`likelier`] -- the likely-types analysis: one incremental fixpoint +//! over constraints generated once per function, with calling context as +//! part of edge identity. Its output is [`facts::LikelyFacts`], the whole +//! contract between analysis and translator. +//! - [`wasm`] -- the translator. `wasm::bbv` is the one codegen path: +//! workqueue basic-block versioning, which lowers each bytecode op in as +//! many type-specialized versions as the program actually reaches, so a +//! failed speculation is an ordinary edge to a differently-typed version +//! rather than a deoptimization event. +//! - [`opsem`] -- the operator-semantics vocabulary (result types, numeric +//! ranges, interval arithmetic) that the analysis and the lowering share, +//! so both reason about `+` in the same words. +//! +//! Output is either an in-process batch of function bodies compiled into a +//! live engine ([`wasm::inprocess`], entered at [`night_inproc_build`]) or a +//! standalone module produced by the snapshot compiler in +//! `js/src/night/nightmonkey`. + +pub mod bytecode; +pub mod constants; +pub mod disasm; +pub mod env_regions; +pub mod facts; +pub mod ids; +pub mod likelier; +pub mod opsem; +pub mod options; +pub mod region_shape; +pub mod source; +pub mod view; +pub mod wasm; + +pub use options::{Diagnostics, Options}; + +/// Build an in-process AOT batch for the `Source` graph at `analysis_source` +/// (root `root_id`): compiled function blobs in wasm-jit-runner format, the +/// extern (helper) table-index array, the compiled-script map, and the +/// serialized environment descriptor. `helper_*` describe the engine helpers +/// (parallel arrays of length `n_helpers`): NUL-terminated name, +/// NUL-terminated signature string (see night_compiler.h), and live funcref-table +/// index. `table_base` is the current table size (`wasm_table_size()`); +/// blob `i` is predicted at `table_base + i`. `alloc` is called exactly +/// twice and must return zeroed, 8-aligned, non-null memory (calloc-style; +/// it may be called with size 0). Returns null on failure (message on +/// stderr); free with `night_inproc_delete`. +/// +/// # Safety +/// `helper_names`/`helper_sigs` must point to `n_helpers` valid +/// NUL-terminated strings and `helper_funcptrs` to `n_helpers` u32s. +#[no_mangle] +pub unsafe extern "C" fn night_inproc_build( + analysis_source: &source::Source, + root_id: u32, + helper_names: *const *const core::ffi::c_char, + helper_sigs: *const *const core::ffi::c_char, + helper_funcptrs: *const u32, + n_helpers: u32, + table_base: u32, + alloc: extern "C" fn(usize) -> u32, +) -> *mut wasm::inprocess::InprocOut { + let n = usize::try_from(n_helpers).unwrap(); + let mut specs = Vec::with_capacity(n); + for i in 0..n { + let name = match core::ffi::CStr::from_ptr(*helper_names.add(i)).to_str() { + Ok(s) => s.to_string(), + Err(e) => { + log::error!("night_inproc_build: helper name {i}: {e}"); + return core::ptr::null_mut(); + } + }; + let sig_str = match core::ffi::CStr::from_ptr(*helper_sigs.add(i)).to_str() { + Ok(s) => s, + Err(e) => { + log::error!("night_inproc_build: helper sig {i}: {e}"); + return core::ptr::null_mut(); + } + }; + let sig = match wasm::inprocess::parse_sig_str(sig_str) { + Ok(s) => s, + Err(e) => { + log::error!("night_inproc_build: helper `{name}`: {e}"); + return core::ptr::null_mut(); + } + }; + specs.push(wasm::inprocess::HelperImportSpec { + name, + sig, + table_index: *helper_funcptrs.add(i), + }); + } + let root_id = source::SourceObjectId::new(root_id); + let opts = Options::default(); + let build = move || { + wasm::inprocess::build_inprocess_batch( + analysis_source, + root_id, + &opts, + &specs, + table_base, + &mut |size: u32| { + let p = alloc(usize::try_from(size).unwrap()); + if p == 0 { + Err("in-process arena allocation failed".to_string()) + } else { + Ok(p) + } + }, + ) + }; + // Big-stack discipline: waffle's Wasm backend lowers nested blocks + // recursively, and a large program overflows the default 8 MB main + // stack -- a silent sigsegv. On wasm32-wasi there are no threads; run + // inline on the main stack, which the shell link sizes accordingly + // (-z stack-size). + #[cfg(target_family = "wasm")] + let result = build(); + #[cfg(not(target_family = "wasm"))] + let result = std::thread::scope(|s| { + std::thread::Builder::new() + .name("night_compiler-inproc-build".to_string()) + .stack_size(1 << 30) + .spawn_scoped(s, build) + .expect("spawn night_compiler-inproc-build thread") + .join() + .expect("night_compiler-inproc-build thread panicked") + }); + match result { + Ok(out) => Box::into_raw(Box::new(out)), + Err(e) => { + // First line only: a waffle validation failure appends the whole + // function body, which is megabytes. The C++ side reports only + // "batch build failed", so without this a failure has no reason + // attached at all. + let head = e.lines().next().unwrap_or(""); + crate::diag_line!("night: inprocess: {head}"); + log::error!("night_inproc_build: {e}"); + core::ptr::null_mut() + } + } +} + +#[no_mangle] +pub extern "C" fn night_inproc_num_blobs(out: &wasm::inprocess::InprocOut) -> u32 { + u32::try_from(out.blobs.len()).unwrap() +} + +#[no_mangle] +pub extern "C" fn night_inproc_blob_ptr(out: &wasm::inprocess::InprocOut, i: u32) -> *const u8 { + out.blobs[i as usize].as_ptr() +} + +#[no_mangle] +pub extern "C" fn night_inproc_blob_len(out: &wasm::inprocess::InprocOut, i: u32) -> u32 { + u32::try_from(out.blobs[i as usize].len()).unwrap() +} + +#[no_mangle] +pub extern "C" fn night_inproc_num_externs(out: &wasm::inprocess::InprocOut) -> u32 { + u32::try_from(out.extern_table_indices.len()).unwrap() +} + +#[no_mangle] +pub extern "C" fn night_inproc_extern_indices(out: &wasm::inprocess::InprocOut) -> *const u32 { + out.extern_table_indices.as_ptr() +} + +#[no_mangle] +pub extern "C" fn night_inproc_num_scripts(out: &wasm::inprocess::InprocOut) -> u32 { + u32::try_from(out.scripts.len()).unwrap() +} + +#[no_mangle] +pub extern "C" fn night_inproc_script_source_id(out: &wasm::inprocess::InprocOut, i: u32) -> u32 { + out.scripts[i as usize].0 +} + +#[no_mangle] +pub extern "C" fn night_inproc_script_blob(out: &wasm::inprocess::InprocOut, i: u32) -> u32 { + out.scripts[i as usize].1 +} + +#[no_mangle] +pub extern "C" fn night_inproc_env_desc_ptr(out: &wasm::inprocess::InprocOut) -> *const u8 { + out.env_desc.as_ptr() +} + +#[no_mangle] +pub extern "C" fn night_inproc_env_desc_len(out: &wasm::inprocess::InprocOut) -> u32 { + u32::try_from(out.env_desc.len()).unwrap() +} + +#[no_mangle] +pub extern "C" fn night_inproc_delete(_out: Box) {} diff --git a/js/src/night/compiler/src/likelier/builtins.rs b/js/src/night/compiler/src/likelier/builtins.rs new file mode 100644 index 0000000000000..a067055267858 --- /dev/null +++ b/js/src/night/compiler/src/likelier/builtins.rs @@ -0,0 +1,621 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! What the analysis knows about the JavaScript builtins it does not have +//! bytecode for. +//! +//! The snapshot walker transcribes registered heap objects, so a native +//! function arrives as an opaque value with a name and nothing else. Three +//! kinds of spec knowledge fill that in, and they all live here so that +//! adding a builtin is one edit in one file: +//! +//! - result masks: what a named native returns (`native_result_for`), and +//! which names preserve integrality (`integral_native` and friends); +//! - namespaces: the synthetic method/constant tables for `Math`, `JSON` +//! and the rest, which the walker cannot transcribe at all +//! (`NAMESPACES`); +//! - constructor names: the array and typed-array constructors, whose +//! calls carry allocation semantics rather than a result mask +//! (`ta_kind_for_ctor_name`, `is_array_ctor_name`), and the natives the +//! translator has an inline arm for (`has_translator_arm`). +//! +//! Every claim here is likely, not proven: each consumer guards the callee +//! identity at runtime, so a program that shadows `Math` or replaces +//! `String.prototype.trim` simply misses its fast path. + +use super::types::{FnId, NameId}; +use crate::opsem::{ + Prims, TaKind, PRIM_BOOLEAN, PRIM_DOUBLE, PRIM_INT32, PRIM_STRING, PRIM_SYMBOL, PRIM_UNDEFINED, +}; +use rustc_hash::FxHashMap as HashMap; + +/// Which receiver a native name was resolved against. The receiver-typed +/// tables overlay the bare one, which is what lets `slice` mean +/// `String.prototype.slice` off a known-string receiver while staying +/// unmodeled off an unknown one. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub enum NativeKind { + /// A bare name: a captured global native or a namespace member. + Bare, + /// A method called on a known-string receiver. + StringMethod, + /// A method called on a known-numeric receiver. + NumberMethod, +} + +/// Spec-derived call result masks for named natives (the EcmaScript spec +/// tells us these; modeling them beats teaching the rest of the analysis +/// to tolerate their absence). Only primitive-returning natives belong +/// here; object-returning or ambiguously-named natives (slice, concat, +/// split, exec, valueOf, ...) stay out and raise unresolved evidence at calls. +/// Names are matched on the native's own function name, which cannot +/// collide with user functions (those always carry scripts). +const NUM: Prims = PRIM_INT32.or(PRIM_DOUBLE); +const NATIVE_RESULTS: &[(&str, Prims)] = &[ + // Math.* (floor/ceil/round return integer-valued doubles the engine + // may box as int32; random can yield 0 -> int32 box). + ("abs", NUM), + ("floor", NUM), + ("ceil", NUM), + ("round", NUM), + ("trunc", NUM), + ("sqrt", NUM), + ("cbrt", NUM), + ("pow", NUM), + ("exp", NUM), + ("expm1", NUM), + ("log", NUM), + ("log2", NUM), + ("log10", NUM), + ("log1p", NUM), + ("sin", NUM), + ("cos", NUM), + ("tan", NUM), + ("asin", NUM), + ("acos", NUM), + ("atan", NUM), + ("atan2", NUM), + ("sinh", NUM), + ("cosh", NUM), + ("tanh", NUM), + ("asinh", NUM), + ("acosh", NUM), + ("atanh", NUM), + ("min", NUM), + ("max", NUM), + ("random", NUM), + ("sign", NUM), + ("fround", NUM), + ("hypot", NUM), + ("imul", PRIM_INT32), + ("clz32", PRIM_INT32), + // Self-hosted intrinsics (GetIntrinsic references): spec-primitive + // kernels the self-hosted string code bottoms out in. + ("Substring", PRIM_STRING), + ("ToString", PRIM_STRING), + ("IsObject", PRIM_BOOLEAN), + ("ToLength", NUM), + ("ToInteger", NUM), + ("Number_isNaN", PRIM_BOOLEAN), + ("UnsafeGetStringFromReservedSlot", PRIM_STRING), + ("UnsafeGetInt32FromReservedSlot", PRIM_INT32), + ("RegExpSearcher", PRIM_INT32), + ("RegExpSearcherLastLimit", PRIM_INT32), + ("RegExpHasCaptureGroups", PRIM_BOOLEAN), + ("RegExpGetSubstitution", PRIM_STRING), + ("IsOptimizableRegExpObject", PRIM_BOOLEAN), + ("SubstringKernel", PRIM_STRING), + ("IsCallable", PRIM_BOOLEAN), + ("AdvanceStringIndex", NUM), + ("ThrowIncompatibleMethod", PRIM_UNDEFINED), + ("ThrowTypeError", PRIM_UNDEFINED), + ("print", PRIM_UNDEFINED), + // Global value converters and predicates. + ("parseInt", NUM), + ("parseFloat", NUM), + ("isNaN", PRIM_BOOLEAN), + ("isFinite", PRIM_BOOLEAN), + ("isInteger", PRIM_BOOLEAN), + ("isSafeInteger", PRIM_BOOLEAN), + ("Number", NUM), + ("String", PRIM_STRING), + ("Boolean", PRIM_BOOLEAN), + ("Symbol", PRIM_SYMBOL), + ("for", PRIM_SYMBOL), + // Date called as a function returns a string (construct is handled + // separately and yields unknown). + ("Date", PRIM_STRING), + ("escape", PRIM_STRING), + ("unescape", PRIM_STRING), + ("encodeURI", PRIM_STRING), + ("decodeURI", PRIM_STRING), + ("encodeURIComponent", PRIM_STRING), + ("decodeURIComponent", PRIM_STRING), + // String.prototype (and statics). + ("charAt", PRIM_STRING), + ("charCodeAt", NUM), + ("codePointAt", NUM.or(PRIM_UNDEFINED)), + ("fromCharCode", PRIM_STRING), + ("fromCodePoint", PRIM_STRING), + ("indexOf", NUM), + ("lastIndexOf", NUM), + ("search", NUM), + ("includes", PRIM_BOOLEAN), + ("startsWith", PRIM_BOOLEAN), + ("endsWith", PRIM_BOOLEAN), + ("localeCompare", NUM), + ("substring", PRIM_STRING), + ("substr", PRIM_STRING), + ("toLowerCase", PRIM_STRING), + ("toUpperCase", PRIM_STRING), + ("toLocaleLowerCase", PRIM_STRING), + ("toLocaleUpperCase", PRIM_STRING), + ("trim", PRIM_STRING), + ("trimStart", PRIM_STRING), + ("trimEnd", PRIM_STRING), + ("repeat", PRIM_STRING), + ("padStart", PRIM_STRING), + ("padEnd", PRIM_STRING), + ("normalize", PRIM_STRING), + ("replace", PRIM_STRING), + ("replaceAll", PRIM_STRING), + // Array.prototype (names not shared with differently-typed peers). + ("join", PRIM_STRING), + ("every", PRIM_BOOLEAN), + ("some", PRIM_BOOLEAN), + // Number.prototype formatters; every toString returns a string. + ("toFixed", PRIM_STRING), + ("toPrecision", PRIM_STRING), + ("toExponential", PRIM_STRING), + ("toString", PRIM_STRING), + ("toLocaleString", PRIM_STRING), + // Object.prototype predicates. + ("hasOwnProperty", PRIM_BOOLEAN), + ("isPrototypeOf", PRIM_BOOLEAN), + ("propertyIsEnumerable", PRIM_BOOLEAN), + // Object statics / Array statics (name-unambiguous predicates). + ("is", PRIM_BOOLEAN), + ("isArray", PRIM_BOOLEAN), + ("isFrozen", PRIM_BOOLEAN), + ("isSealed", PRIM_BOOLEAN), + ("isExtensible", PRIM_BOOLEAN), + // RegExp / json. ("parse" stays out: Date.parse is numeric but + // Json.parse returns anything.) + ("test", PRIM_BOOLEAN), + ("stringify", PRIM_STRING.or(PRIM_UNDEFINED)), + // Date: getters/setters return timestamps or components (NaN for + // invalid dates -> double side), the to*String family strings. + ("now", NUM), + ("UTC", NUM), + ("getTime", NUM), + ("getFullYear", NUM), + ("getMonth", NUM), + ("getDate", NUM), + ("getDay", NUM), + ("getHours", NUM), + ("getMinutes", NUM), + ("getSeconds", NUM), + ("getMilliseconds", NUM), + ("getTimezoneOffset", NUM), + ("getYear", NUM), + ("getUTCFullYear", NUM), + ("getUTCMonth", NUM), + ("getUTCDate", NUM), + ("getUTCDay", NUM), + ("getUTCHours", NUM), + ("getUTCMinutes", NUM), + ("getUTCSeconds", NUM), + ("getUTCMilliseconds", NUM), + ("setTime", NUM), + ("setFullYear", NUM), + ("setMonth", NUM), + ("setDate", NUM), + ("setHours", NUM), + ("setMinutes", NUM), + ("setSeconds", NUM), + ("setMilliseconds", NUM), + ("setYear", NUM), + ("toDateString", PRIM_STRING), + ("toTimeString", PRIM_STRING), + ("toISOString", PRIM_STRING), + ("toUTCString", PRIM_STRING), + ("toGMTString", PRIM_STRING), + ("toLocaleDateString", PRIM_STRING), + ("toLocaleTimeString", PRIM_STRING), + ("toSource", PRIM_STRING), + ("trimLeft", PRIM_STRING), + ("trimRight", PRIM_STRING), + ("isWellFormed", PRIM_BOOLEAN), + ("toWellFormed", PRIM_STRING), +]; + +/// String.prototype methods, keyed by a known-string receiver -- which +/// disambiguates names the bare table must skip (slice/concat/at/valueOf +/// all return strings here). +const STRING_METHOD_RESULTS: &[(&str, Prims)] = &[ + ("slice", PRIM_STRING), + ("concat", PRIM_STRING), + ("at", PRIM_STRING.or(PRIM_UNDEFINED)), + ("valueOf", PRIM_STRING), +]; + +/// Number.prototype methods under a known-numeric receiver. +const NUMBER_METHOD_RESULTS: &[(&str, Prims)] = &[("valueOf", NUM)]; + +/// Whether a UTF-16 property name equals a source literal. +/// +/// Property names arrive from the engine as UTF-16 and every name this +/// module knows is written as a Rust `&str`, so the comparison is the most +/// repeated line in it. +pub(super) fn name_eq(name: &[u16], s: &str) -> bool { + name.iter().copied().eq(s.encode_utf16()) +} + +fn lookup(table: &[(&str, Prims)], name: &[u16]) -> Option { + table + .iter() + .find(|(n, _)| name_eq(name, n)) + .map(|&(_, m)| m) +} + +/// Integral-result natives: the value, when a Number, is an integer. These +/// are what carry an i53 claim through a `Math.floor` chain. NaN is the +/// optimism-with-guards case, same as the arith ladder. +pub(super) fn integral_native(name: &[u16]) -> bool { + ["floor", "ceil", "round", "trunc", "parseInt"] + .iter() + .any(|n| name_eq(name, n)) +} + +/// Integrality-preserving natives: the result is integral iff every +/// argument is. abs/min/max are exact; pow's fractional cases (negative +/// or non-integral exponent) are the guarded rare ones at the Likely +/// stance -- pow(int, int>=0) is always integral (every IEEE double at +/// or above 2^52 is an integer; below that the result is exact). Without +/// this, one cold `Math.pow` feeding a bignum constructor ranges every +/// digit cell in the library to Top. +pub(super) fn integral_preserving_native(name: &[u16]) -> bool { + ["pow", "abs", "min", "max"] + .iter() + .any(|n| name_eq(name, n)) +} + +/// The spec result mask of a native name resolved against `kind`, or +/// `None` for names this module does not model. +pub(super) fn native_result_for(kind: NativeKind, name: &[u16]) -> Option { + let overlay = match kind { + NativeKind::StringMethod => lookup(STRING_METHOD_RESULTS, name), + NativeKind::NumberMethod => lookup(NUMBER_METHOD_RESULTS, name), + NativeKind::Bare => None, + }; + overlay.or_else(|| lookup(NATIVE_RESULTS, name)) +} + +/// What a modeled native may write to pre-existing heap. `Pure` claims no +/// such writes (fresh allocation is allowed; argument coercion can still +/// reach user code, which is why summary consumers keep only +/// guarded/recoverable state); `Elems` writes only its receiver's +/// elements/length. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NativeEffect { + Pure, + Elems, + Top, +} + +/// Effect class of a native name resolved against `kind`: the modeled +/// primitive-returning names are `Pure` (`has_result` carries the +/// interned mask's presence, so mangled intrinsic ids classify by their +/// resolved result rather than by re-looking up the mangled name), the +/// in-place array mutators `Elems`, the allocation-only constructors and +/// object-returning pure kernels `Pure` by name, everything else `Top` +/// (`sort` stays `Top`: the comparator is user code). +pub(super) fn native_effect(kind: NativeKind, name: &[u16], has_result: bool) -> NativeEffect { + const ELEMS: &[&str] = &[ + "push", + "pop", + "shift", + "unshift", + "fill", + "copyWithin", + "splice", + "reverse", + ]; + // No writes to pre-existing heap: fresh allocations and coercion + // kernels (argument coercion reaching user code is tolerated by the + // summary contract, same as every other Pure name). + const PURE: &[&str] = &[ + "Error", + "TypeError", + "RangeError", + "ReferenceError", + "SyntaxError", + "EvalError", + "URIError", + "RegExp", + "ArrayBuffer", + "create", + "keys", + "values", + "entries", + "freeze", + "seal", + "getPrototypeOf", + "getOwnPropertyNames", + "getOwnPropertyDescriptor", + "%ToObject", + "%RegExpMatcher", + "%StringSplitString", + "%GuardToSetObject", + "%GuardToMapObject", + ]; + if ELEMS.iter().any(|n| name_eq(name, n)) { + NativeEffect::Elems + } else if has_result + || native_result_for(kind, name).is_some() + || PURE.iter().any(|n| name_eq(name, n)) + { + NativeEffect::Pure + } else { + NativeEffect::Top + } +} + +/// Whether a name is a plausible method of the given prim-receiver kind +/// (drives callee-position resolution off string/number receivers; only +/// modeled names resolve -- an absent name leaves the cell Empty). +pub(super) fn prim_method(kind: NativeKind, name: &[u16]) -> bool { + native_result_for(kind, name).is_some() + && match kind { + NativeKind::StringMethod => { + lookup(STRING_METHOD_RESULTS, name).is_some() + || STRING_PROTO_NAMES.iter().any(|n| name_eq(name, n)) + } + NativeKind::NumberMethod => { + lookup(NUMBER_METHOD_RESULTS, name).is_some() + || NUMBER_PROTO_NAMES.iter().any(|n| name_eq(name, n)) + } + NativeKind::Bare => false, + } +} + +/// The bare-table names that genuinely live on String.prototype (the +/// bare table also holds Math/Date/... names a string receiver must not +/// resolve). +const STRING_PROTO_NAMES: &[&str] = &[ + "charAt", + "charCodeAt", + "codePointAt", + "indexOf", + "lastIndexOf", + "search", + "includes", + "startsWith", + "endsWith", + "localeCompare", + "substring", + "substr", + "toLowerCase", + "toUpperCase", + "toLocaleLowerCase", + "toLocaleUpperCase", + "trim", + "trimStart", + "trimEnd", + "trimLeft", + "trimRight", + "repeat", + "padStart", + "padEnd", + "normalize", + "replace", + "replaceAll", + "toString", + "isWellFormed", + "toWellFormed", +]; + +const NUMBER_PROTO_NAMES: &[&str] = &[ + "toFixed", + "toPrecision", + "toExponential", + "toString", + "toLocaleString", +]; + +/// Synthesized builtin namespaces: the walker transcribes only registered +/// heap objects, so `Math` and friends arrive as other values. When the +/// global's binding is absent-or-other we seed a synthetic abstraction +/// whose field cells hold native fn ids (methods) and prim masks +/// (constants); program monkeypatches join into the same cells. `ctor` +/// gives the namespace value itself a callable native id (String(x), +/// Number(x), Date() -> string). +pub(super) struct NsSpec { + pub global: &'static str, + /// The namespace value is itself callable (`String(x)`, `Number(x)`). + pub ctor: bool, + pub methods: &'static [&'static str], + pub consts: &'static [(&'static str, Prims)], +} + +const D: Prims = PRIM_DOUBLE; +pub(super) const NAMESPACES: &[NsSpec] = &[ + NsSpec { + global: "Math", + ctor: false, + methods: &[ + "abs", "floor", "ceil", "round", "trunc", "sqrt", "cbrt", "pow", "exp", "expm1", "log", + "log2", "log10", "log1p", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", + "cosh", "tanh", "asinh", "acosh", "atanh", "min", "max", "random", "sign", "fround", + "hypot", "imul", "clz32", + ], + consts: &[ + ("E", D), + ("LN10", D), + ("LN2", D), + ("LOG10E", D), + ("LOG2E", D), + ("PI", D), + ("SQRT1_2", D), + ("SQRT2", D), + ], + }, + NsSpec { + global: "JSON", + ctor: false, + methods: &["stringify"], + consts: &[], + }, + NsSpec { + global: "String", + ctor: true, + methods: &["fromCharCode", "fromCodePoint"], + consts: &[], + }, + NsSpec { + global: "Number", + ctor: true, + methods: &[ + "isInteger", + "isNaN", + "isFinite", + "isSafeInteger", + "parseInt", + "parseFloat", + ], + consts: &[ + ("POSITIVE_INFINITY", D), + ("NEGATIVE_INFINITY", D), + ("MAX_VALUE", D), + ("MIN_VALUE", D), + ("MAX_SAFE_INTEGER", D), + ("MIN_SAFE_INTEGER", D), + ("EPSILON", D), + ("NaN", D), + ], + }, + NsSpec { + global: "Boolean", + ctor: true, + methods: &[], + consts: &[], + }, + NsSpec { + global: "Symbol", + ctor: true, + methods: &["for"], + consts: &[], + }, + NsSpec { + global: "Date", + ctor: true, + methods: &["now", "UTC"], + consts: &[], + }, + NsSpec { + global: "performance", + ctor: false, + methods: &["now"], + consts: &[], + }, + // defineProperty feeds the accessor table (calls.rs + // eval_define_property); the other statics resolve to Bare natives + // whose calls yield unknown evidence -- an unresolved callee left + // the ret cell EMPTY, and Empty is worse than unknown (pdfjs builds + // its dicts with Object.create; every consumer read as no-value). + NsSpec { + global: "Object", + ctor: true, + methods: &[ + "defineProperty", + "create", + "keys", + "values", + "entries", + "assign", + "freeze", + "seal", + "getPrototypeOf", + "getOwnPropertyNames", + "getOwnPropertyDescriptor", + ], + consts: &[], + }, +]; + +/// One modeled native, as the analysis knows it: a single struct rather +/// than parallel vectors indexed by a shared offset, so kind, name and +/// result cannot fall out of step. +pub struct NativeInfo { + pub kind: NativeKind, + pub name: NameId, + /// The spec result mask, or `None` for a native this module does not + /// model (whose calls raise unresolved evidence). + pub result: Option, +} + +/// The reserved function ids minted for named natives. +#[derive(Default)] +pub struct Natives { + /// Indexed by [`FnId::native_index`]. + by_index: Vec, + ids: HashMap<(NativeKind, NameId), FnId>, +} + +impl Natives { + /// Get-or-mint the reserved id for a `(kind, name)` native, resolving + /// its spec result mask once. + pub fn intern(&mut self, kind: NativeKind, name: NameId, chars: &[u16]) -> FnId { + if let Some(&id) = self.ids.get(&(kind, name)) { + return id; + } + let id = FnId::native(u32::try_from(self.by_index.len()).unwrap()); + self.by_index.push(NativeInfo { + kind, + name, + result: native_result_for(kind, chars), + }); + self.ids.insert((kind, name), id); + id + } + + pub fn get(&self, f: FnId) -> Option<&NativeInfo> { + self.by_index.get(f.native_index()? as usize) + } +} + +/// The typed-array constructor a name denotes, if any. +pub(super) fn ta_kind_for_ctor_name(name: &[u16]) -> Option { + const NAMES: [(&str, TaKind); 9] = [ + ("Int8Array", TaKind::Int8), + ("Uint8Array", TaKind::Uint8), + ("Uint8ClampedArray", TaKind::Uint8Clamped), + ("Int16Array", TaKind::Int16), + ("Uint16Array", TaKind::Uint16), + ("Int32Array", TaKind::Int32), + ("Uint32Array", TaKind::Uint32), + ("Float32Array", TaKind::Float32), + ("Float64Array", TaKind::Float64), + ]; + NAMES + .iter() + .find(|(s, _)| name_eq(name, s)) + .map(|&(_, k)| k) +} + +/// Whether a name denotes the `Array` constructor. +pub(super) fn is_array_ctor_name(name: &[u16]) -> bool { + name_eq(name, "Array") +} + +/// Whether the translator has an inline arm for this bare-name native. +/// A site that resolves to one of these gets the `native_calls` fact; the +/// rest keep their spec result mask but go through the generic call path. +pub(super) fn has_translator_arm(name: &[u16]) -> bool { + const ARMED: [&str; 14] = [ + "max", "min", "pow", "sqrt", "abs", "floor", "ceil", "trunc", "fround", "imul", "clz32", + "sin", "cos", "parseInt", + ]; + ARMED.iter().any(|n| name_eq(name, n)) +} diff --git a/js/src/night/compiler/src/likelier/calls.rs b/js/src/night/compiler/src/likelier/calls.rs new file mode 100644 index 0000000000000..ab29694de36f0 --- /dev/null +++ b/js/src/night/compiler/src/likelier/calls.rs @@ -0,0 +1,1035 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Call binding, contexts, construct semantics, apply delegation, escape. +//! +//! A context is one interned id (a bounded call string), not a graph copy -- +//! nothing is cloned. Binding a call reads the argument cells (subscribing, +//! so growth re-binds) and raises the callee's per-ctx Arg/This rows; the +//! callee's constraints are instantiated lazily the first time a ctx +//! reaches it. Degradations are explicit and censused, never silent: +//! depth cap, callee cap, recursion collapse, global budget -- all bind +//! into the callee's generic context instead. + +use super::engine::{CellKey, ConId, Constraint, SEED}; +use super::stats::Stats; +use super::types::{BoundedFnSet, CtxId, FnId, ObjType, TypeSet, CTX0}; +use super::Solver; +use crate::constants::{ + CALLEE_CAP, CTX_BUDGET, CTX_DEPTH_CAP, MAX_TRACKED_FORMALS, TABLE_MEMBER_CAP, +}; +use crate::facts::CallForm; +use crate::ids::{FormalIndex, Pc, ScriptId, Site}; +use rustc_hash::FxHashMap as HashMap; +use rustc_hash::FxHashSet as HashSet; + +/// One frame of an interned call string. The chain of `parent` links from +/// a `CtxId` back to `CTX0` spells out the calls that reached it. +struct CtxFrame { + parent: CtxId, + /// The callee script this frame entered. Not "the callee" of anything + /// -- a script calls many others, and each such call gets its own + /// frame; this is the one call this frame stands for. Recursion + /// collapse walks the parent chain looking for a frame that already + /// entered the same script. `None` in frame 0, the generic context. + callee: Option, + depth: u8, +} + +pub struct Ctxs { + frames: Vec, + ids: HashMap<(CtxId, Site, ScriptId), CtxId>, + pub depth_cap: u8, + pub callee_cap: usize, + pub budget: u64, +} + +impl Default for Ctxs { + fn default() -> Self { + Self::new() + } +} + +impl Ctxs { + pub fn new() -> Ctxs { + Ctxs { + // Frame 0 is the generic context. + frames: vec![CtxFrame { + parent: CTX0, + callee: None, + depth: 0, + }], + ids: HashMap::default(), + depth_cap: CTX_DEPTH_CAP, + callee_cap: CALLEE_CAP, + budget: CTX_BUDGET, + } + } + + /// Mint (or reuse) the context for entering `callee` from `site` at + /// `ctx`. Returns CTX0 (with a census tick) on any degradation. + fn push(&mut self, ctx: CtxId, site: Site, callee: ScriptId, stats: &mut Stats) -> CtxId { + // Recursion collapse: if the callee already appears in the chain, + // reuse the context it was entered at (SCCs context-insensitive + // internally). + let mut cur = ctx; + while cur != CTX0 { + let f = &self.frames[cur.0 as usize]; + if f.callee == Some(callee) { + stats.call_ctx_degraded_recursion += 1; + return cur; + } + cur = f.parent; + } + let depth = self.frames[ctx.0 as usize].depth; + if depth >= self.depth_cap { + stats.call_ctx_degraded_depth += 1; + return CTX0; + } + if stats.ctxs_spent >= self.budget { + stats.call_ctx_degraded_budget += 1; + return CTX0; + } + if let Some(&c) = self.ids.get(&(ctx, site, callee)) { + return c; + } + let c = CtxId(u32::try_from(self.frames.len()).unwrap()); + self.frames.push(CtxFrame { + parent: ctx, + callee: Some(callee), + depth: depth + 1, + }); + self.ids.insert((ctx, site, callee), c); + c + } + + /// Every (caller ctx, site) edge that mints a context, by context. + pub fn enter_sites(&self) -> HashMap> { + let mut out: HashMap> = HashMap::default(); + for (&(_, site, _), &c) in &self.ids { + out.entry(c).or_default().push(site); + } + out + } + + /// Human-readable provenance of a context, for the site tracer: its + /// depth, parent, and every (caller-ctx, site) edge that mints it. + pub fn describe(&self, ctx: CtxId) -> String { + if ctx == CTX0 { + return "generic".to_string(); + } + let f = &self.frames[ctx.0 as usize]; + let mut vias: Vec = self + .ids + .iter() + .filter(|&(_, &v)| v == ctx) + .map(|(&(p, site, _), _)| format!("ctx {} at {site}", p.0)) + .collect(); + vias.sort(); + format!( + "depth {} parent {} via [{}]", + f.depth, + f.parent.0, + vias.join("; ") + ) + } +} + +/// One call constraint being evaluated: who is calling, at which context, +/// on behalf of which firing, and where the result goes. Every binding step +/// below needs all four, and threading them as one value is what lets the +/// three call shapes (ordinary call, `new`, `.call`/`.apply`) share their +/// binding code instead of restating it. +#[derive(Clone, Copy)] +struct CallAt { + /// The calling script and the context it is being evaluated at. + script: ScriptId, + ctx: CtxId, + /// The firing doing the reading, so reads subscribe to it. + user: (ConId, CtxId), + /// Where the call's result goes, in the caller's frame. + ret: super::engine::CKey, +} + +impl Solver<'_> { + /// Bind the caller's arguments into `f`'s formal rows at `cx`, dropping + /// the first `skip` (a `.call` site's leading receiver, which is the + /// forwarded `this` rather than a formal). + fn bind_args( + &mut self, + at: CallAt, + f: ScriptId, + cx: CtxId, + args: &[super::engine::CKey], + skip: usize, + ) { + for (i, a) in args.iter().skip(skip).enumerate() { + let src = self.engine.resolve(at.script, at.ctx, *a); + let v = self.engine.read(src, at.user); + let arg = FormalIndex::new(u32::try_from(i).unwrap()); + self.note_arg_fn(f, arg, &v); + let dst = self.engine.cell(CellKey::Arg { + script: f, + arg, + ctx: cx, + }); + self.engine.raise(dst, &v, at.user); + } + } + + /// Bind `tk` into `f`'s receiver row at `cx`, unless the this-assertion + /// refuses it. A caller handing over its own `this` also records the + /// delegation edge, so `f`'s this-writes attribute to the caller's home + /// classes. + /// + /// `method_recv`: `tk` is a method call's receiver (`x.m()`), so a + /// nullish value never reaches the callee -- the property access + /// throws first, in both modes. Stripping null/undefined here is what + /// keeps a null-seeded field (`this.root_ = null`) from widening the + /// callee's This cell past the object claim its every actual + /// invocation satisfies. An explicit `.call/.apply` this is NOT this + /// shape: a strict callee really can observe null there, so those + /// sites pass false. + fn bind_this( + &mut self, + at: CallAt, + f: ScriptId, + cx: CtxId, + tk: super::engine::CKey, + method_recv: bool, + ) { + if tk == super::engine::CKey::This { + self.this_deleg_add(at.script, f); + } + let src = self.engine.resolve(at.script, at.ctx, tk); + let mut v = self.engine.read(src, at.user); + if method_recv { + v.prims = v.prims - (crate::opsem::PRIM_NULL | crate::opsem::PRIM_UNDEFINED); + if v.is_empty() { + // A provably-nullish receiver: the call never happens. + return; + } + } + // A worse-than-asserted receiver (AnyObject/AnyOf into a pinned + // method) must not leave the context's This EMPTY -- an empty This + // reads as "never invoked" and every this-dependent value in the + // body computes nothing at this context (the callee's return dies + // with it). The per-site context's cells are separate from CTX0's, + // so binding here cannot destroy the assertion the refusal + // protects: a single-owner pin binds its asserted class (strictly + // better evidence than the lost receiver), a conflicted pin (a + // genuinely shared method) binds the receiver itself. + let bound = if self.bind_this_ok(f, &v) { + v + } else if let Some(&owner) = self.this_pin.get(&f).and_then(super::types::Agreed::get) { + TypeSet { + obj: ObjType::ClassAny(owner), + ..TypeSet::default() + } + } else { + v + }; + let dst = self.engine.cell(CellKey::This { script: f, ctx: cx }); + self.engine.raise(dst, &bound, at.user); + } + + /// Propagate `f`'s return at `cx` into the call's result. + fn bind_ret(&mut self, at: CallAt, f: ScriptId, cx: CtxId) { + let ret_src = self.engine.cell(CellKey::Ret { script: f, ctx: cx }); + let v = self.engine.read(ret_src, at.user); + let ret_dst = self.engine.resolve(at.script, at.ctx, at.ret); + self.engine.raise(ret_dst, &v, at.user); + } + + /// Raise the result of calling a callee that is not a script: a modeled + /// native's spec mask, or unknown evidence for anything unmodeled. + fn raise_builtin_ret(&mut self, at: CallAt, f: FnId, args: &[super::engine::CKey]) { + let v = if f.native_index().is_some() { + let ai = self.args_integral(at.script, at.ctx, args, at.user); + self.native_ret(f, ai) + } else { + TypeSet::unknown_evidence() + }; + let ret_dst = self.engine.resolve(at.script, at.ctx, at.ret); + self.engine.raise(ret_dst, &v, at.user); + } +} + +impl Solver<'_> { + /// Every argument at the site is integrally ranged (numeric prims + /// only, range at or below I53) -- the integral-preserving native + /// condition. Reads subscribe, so a later widening re-evals the call. + fn args_integral( + &mut self, + script: ScriptId, + ctx: CtxId, + args: &[super::engine::CKey], + user: (ConId, CtxId), + ) -> bool { + args.iter().all(|&a| { + let cell = self.engine.resolve(script, ctx, a); + let ts = self.engine.read(cell, user); + ts.prims + .subset_of(crate::opsem::PRIM_INT32 | crate::opsem::PRIM_DOUBLE) + && ts.fns.is_empty() + && ts.obj == ObjType::Empty + && ts.range <= super::types::Range::I53 + }) + } + + pub(super) fn eval_call(&mut self, con: ConId, ctx: CtxId) -> bool { + let script = self.engine.con_script[con.0 as usize]; + let user = (con, ctx); + match self.engine.cons[con.0 as usize].clone() { + Constraint::Call { + callee, + this_, + args, + ret, + pc, + construct, + } => { + let at = CallAt { + script, + ctx, + user, + ret, + }; + let c = self.engine.resolve(script, ctx, callee); + let cts = self.engine.read(c, user); + self.note_site_calls(script, pc, &cts); + if construct { + self.note_site_ctor_native(script, pc, &cts.fns); + } else { + self.note_site_native(script, pc, &cts.fns); + } + let region_fed = matches!(callee, super::engine::CKey::Var(v) + if self.region_calls.contains(&(script, v))); + if region_fed { + // Region-resolved dispatch: the callee set here came + // from a region's method tables (`region_methods`) + // rather than from a resolved function value, so it is + // a guess at which of several sibling classes' methods + // this site reaches. The set itself is already recorded + // as the site's guard chain above; what is left is + // whether to bind arguments into those callees, and at + // which context. + // + // Binding is worth doing -- the facts it produces inside + // the guessed bodies are what makes the spliced arms + // worth emitting -- but neither obvious context works: + // + // - The generic context (CTX0) is shared by every + // caller of the callee, so a guessed argument + // joined there is visible to every other call of + // that function, forever. One wrong guess widens + // the callee's formals for the whole program. + // - Chaining off the caller's own context is precise, + // but a region dispatch fans out to every member + // class, and each of those may itself dispatch + // through a region. The product exhausts the + // context budget, and once the budget is gone + // `Ctxs::push` degrades *everything* after it to + // CTX0 -- so the precise choice ends up causing the + // same whole-program widening the shared row would + // have, just later and less predictably. + // + // So: a depth-1 context parented at the generic one, + // minted per (site, target). The guess stays contained + // in a row nothing else reads, and the budget sees a + // flat cost rather than a multiplied one. A target the + // budget refuses simply does not bind. + // + // Every resolved target binds -- the fn-set bound and + // the region cap are the population gates, and the ctx + // budget is the cost gate; a second, tighter cap here + // would starve any closed dispatch wider than it, + // since none of its callees' arguments would flow. + if !cts.fns.is_multi() { + for f in cts.fns.ids().to_vec() { + let Some(f) = f.as_script() else { + self.raise_builtin_ret(at, f, &args); + continue; + }; + let cx = + self.ctxs + .push(CTX0, Site::new(script, pc), f, &mut self.stats); + if cx == CTX0 { + self.raise_unknown_ret(script, ctx, ret, user); + continue; + } + if self.engine.instantiate(f, cx) { + self.stats.ctxs_spent += 1; + } + self.bind_args(at, f, cx, &args, 0); + if !construct { + if let Some(tk) = this_ { + self.bind_this(at, f, cx, tk, true); + } + } + self.bind_ret(at, f, cx); + } + } else { + // Megamorphic or over-cap region dispatch: the + // call still executes. + self.raise_unknown_ret(script, ctx, ret, user); + } + return true; + } + // A resolved fn set binds even when the obj part is + // AnyObject (method-union callees ride Any-valued reads); + // only a truly unusable callee escapes the arguments. + // An executed-but-unresolved call raises the unknown + // evidence bit into its result, never nothing: an Empty + // result reads as "no value ever arrived here", so a + // consumer would claim whatever its other, numeric-only + // writers said and miss on every call result. + if cts.fns.is_multi() || (cts.fns.is_empty() && cts.obj == ObjType::AnyObject) { + // Fn-table dispatch: a multi callee read + // off a snapshot fn-table's elems still binds the join + // of the site's arg profiles into every member's Arg + // row at the generic context. Dispatch stays opaque + // (unknown ret, args escape as before) -- only the + // members' formals learn. + if cts.fns.is_multi() { + if let super::engine::CKey::Var(v) = callee { + if let Some(&rk) = self.elems_callee_vars.get(&(script, v)) { + let rcell = self.engine.resolve(script, ctx, rk); + let rts = self.engine.read(rcell, user); + if let ObjType::One(a) = rts.obj { + if self.table_members.contains_key(&a) { + self.bind_table_args(a, script, ctx, &args, user); + } + } + } + } + } + self.raise_unknown_ret(script, ctx, ret, user); + for a in args.iter() { + let ac = self.engine.resolve(script, ctx, *a); + let v = self.engine.read(ac, user); + self.do_escape(&v, user); + } + return true; + } + if cts.fns.is_empty() && matches!(cts.obj, ObjType::AnyOf(_)) { + self.raise_unknown_ret(script, ctx, ret, user); + return true; + } + let targets = cts.fns.ids().to_vec(); + let poly = + targets.iter().filter(|f| !f.is_builtin()).count() > self.ctxs.callee_cap; + for f in targets { + if f.native_index().is_some() { + // A named native: the call result is spec-modeled + // (or unknown); constructing one yields an object + // we do not model. `Object.defineProperty` is the + // one native with modeled heap semantics: it feeds + // the class accessor table. + let v = if construct { + TypeSet::unknown_evidence() + } else if self.is_define_property(f) { + self.eval_define_property(script, ctx, pc, &args, user) + } else { + let ai = self.args_integral(script, ctx, &args, user); + self.native_ret(f, ai) + }; + let ret_dst = self.engine.resolve(script, ctx, ret); + self.engine.raise(ret_dst, &v, user); + continue; + } + let Some(f) = f.as_script() else { + // A builtin constructor value (`var Vector = Array`): + // allocation semantics at this site, call == construct. + let (is_array, ta) = if f == FnId::ARRAY_CTOR { + (true, None) + } else { + (false, f.typed_array_kind()) + }; + let abs = self.intern_alloc(script, pc, ctx, None, is_array, ta); + let ret_dst = self.engine.resolve(script, ctx, ret); + self.engine.raise(ret_dst, &TypeSet::obj_one(abs), user); + continue; + }; + let cx = self.enter(ctx, script, pc, f, poly); + self.bind_args(at, f, cx, &args, 0); + if construct { + let this_cell = self.engine.cell(CellKey::This { script: f, ctx: cx }); + let ret_dst = self.engine.resolve(script, ctx, ret); + self.constructed.insert(f); + // Shared-generated ctors: the site's snapshot- + // resolved per-prototype class, else the script- + // keyed identity. + let class = match self.site_ctor_class.get(&Site::new(script, pc)) { + Some(&c) => c, + None => self.class_for_fn(f), + }; + let abs = self.intern_alloc(script, pc, ctx, Some(class), false, None); + let t = TypeSet::obj_one(abs); + self.engine.raise(this_cell, &t, user); + let ret_src = self.engine.cell(CellKey::Ret { script: f, ctx: cx }); + let rv = self.engine.read(ret_src, user); + if self.tables.explicit_ret.contains(&f) { + // Object-returning constructor: `new F()` yields + // F's return when it is an object (NVector); the + // `this` allocation only where an observed + // primitive return path exists (the unknown bit + // is not one -- construct semantics box it to an + // object either way). Both activations are + // monotone. + let mut objpart = TypeSet { + fns: rv.fns.clone(), + obj: rv.obj, + ..TypeSet::default() + }; + if !rv.prims.is_empty() { + objpart.join_from( + &t, + &self.engine.abs_labels, + &mut self.engine.sink, + ); + } + self.engine.raise(ret_dst, &objpart, user); + } else { + self.engine.raise(ret_dst, &t, user); + } + } else { + if let Some(tk) = this_ { + self.bind_this(at, f, cx, tk, true); + } + self.bind_ret(at, f, cx); + } + } + true + } + Constraint::Apply { + target, + args, + arg1_is_arguments, + ret, + pc, + form, + } => { + let at = CallAt { + script, + ctx, + user, + ret, + }; + let t = self.engine.resolve(script, ctx, target); + let tts = self.engine.read(t, user); + self.note_site_apply(script, ctx, pc, &tts.fns, form); + if tts.fns.is_multi() + || (tts.fns.is_empty() + && matches!(tts.obj, ObjType::AnyObject | ObjType::AnyOf(_))) + { + // Fn-table dispatch through an apply form + // (`action[0].call(scope, data)`): a multi target read + // off a known table's elems still binds the site's arg + // profile and thisArg into every member's rows, so the + // handler bodies stop reading Empty formals. Dispatch + // stays opaque (unknown ret), and each member binds in + // a depth-1 context parented at the generic one -- the + // region-dispatch containment rule (a shared CTX0 row + // would widen every caller of the member forever, and + // caller-chained contexts fan out past the budget). + if tts.fns.is_multi() && form == CallForm::Call { + if let super::engine::CKey::Var(v) = target { + if let Some(&rk) = self.elems_callee_vars.get(&(script, v)) { + let rcell = self.engine.resolve(script, ctx, rk); + let rts = self.engine.read(rcell, user); + let members: Vec = match rts.obj { + ObjType::One(a) => self + .table_members + .get(&a) + .map_or_else(Vec::new, |m| m.iter().copied().collect()), + ObjType::ClassAny(c) => self + .class_table_members + .get(&c) + .map_or_else(Vec::new, |m| m.iter().copied().collect()), + _ => Vec::new(), + }; + let mut members = members; + members.sort_unstable(); + for f in members { + let Some(f) = f.as_script() else { + continue; + }; + let cx = self.ctxs.push( + CTX0, + Site::new(script, pc), + f, + &mut self.stats, + ); + if cx == CTX0 { + continue; + } + if self.engine.instantiate(f, cx) { + self.stats.ctxs_spent += 1; + } + if let Some(&recv) = args.first() { + self.bind_this(at, f, cx, recv, false); + } + self.bind_args(at, f, cx, &args, 1); + } + } + } + } + self.raise_unknown_ret(script, ctx, ret, user); + return true; + } + let targets = tts.fns.ids().to_vec(); + let poly = targets.len() > self.ctxs.callee_cap; + for f in targets { + let Some(f) = f.as_script() else { + self.raise_builtin_ret(at, f, &args); + continue; + }; + let cx = self.enter(ctx, script, pc, f, poly); + if let Some(&recv) = args.first() { + self.bind_this(at, f, cx, recv, false); + } + if form == CallForm::Call { + self.bind_args(at, f, cx, &args, 1); + } else if arg1_is_arguments { + // `T.apply(this, arguments)`: forward the caller's + // own argument rows. + for i in 0..MAX_TRACKED_FORMALS { + let arg = FormalIndex::new(i); + let src = self.engine.cell(CellKey::Arg { script, arg, ctx }); + let v = self.engine.read(src, user); + let dst = self.engine.cell(CellKey::Arg { + script: f, + arg, + ctx: cx, + }); + self.engine.raise(dst, &v, user); + } + } + self.bind_ret(at, f, cx); + } + true + } + _ => false, + } + } + + /// Fn-table arg-binding: raise the dispatch site's arg + /// reads into the table's per-index join rows; standing links fan + /// each row into every member's Arg cell at the generic context. + /// Reads subscribe, so arg growth re-binds; `link` propagates the + /// current row into late-arriving members, so member growth is + /// monotone too. + fn bind_table_args( + &mut self, + a: super::types::AbsId, + script: ScriptId, + ctx: CtxId, + args: &[super::engine::CKey], + user: (ConId, CtxId), + ) { + self.table_bound.entry(a).or_insert(0); + self.install_table_links(a); + for (i, k) in args.iter().enumerate().take(MAX_TRACKED_FORMALS as usize) { + let src = self.engine.resolve(script, ctx, *k); + let v = self.engine.read(src, user); + let j = self.engine.cell(CellKey::TableArgJoin { + abs: a, + arg: FormalIndex::new(u32::try_from(i).unwrap()), + }); + self.engine.raise(j, &v, user); + } + } + + /// Whether native id `f` is the modeled `Object.defineProperty`. + fn is_define_property(&self, f: FnId) -> bool { + self.natives.get(f).is_some_and(|i| { + i.kind == super::builtins::NativeKind::Bare && i.name == self.names_of.define_property + }) + } + + /// Model `Object.defineProperty(target, "name", {get, set})`: register + /// the accessor pair on the target's class (the target is a prototype + /// abstraction or a class-owned concrete prototype), bind the bodies' + /// `this`, and re-fire same-name heap constraints so evaluations that + /// ran before registration route through the accessor. Returns the + /// call result (the target). + fn eval_define_property( + &mut self, + script: ScriptId, + ctx: CtxId, + pc: Pc, + args: &[super::engine::CKey], + user: (ConId, CtxId), + ) -> TypeSet { + let tts = if let Some(&t) = args.first() { + let c = self.engine.resolve(script, ctx, t); + self.engine.read(c, user) + } else { + TypeSet::default() + }; + let ret = if tts.is_empty() { + TypeSet::unknown_evidence() + } else { + tts.clone() + }; + if args.len() != 3 { + return ret; + } + let Some(&name) = self.tables.call_str_arg1.get(&Site::new(script, pc)) else { + return ret; + }; + let ObjType::One(t) = tts.obj else { + return ret; + }; + let info = &self.heap[t]; + let Some(class) = info.proto_of.or(info.owner_class) else { + return ret; + }; + let dcell = self.engine.resolve(script, ctx, args[2]); + let dts = self.engine.read(dcell, user); + let ObjType::One(d) = dts.obj else { + return ret; + }; + self.ensure_seeded(d); + let n_get = self.names_of.get; + let n_set = self.names_of.set; + let gcell = self.field_cell(d, n_get); + let g = self.engine.read(gcell, user); + let scell = self.field_cell(d, n_set); + let s = self.engine.read(scell, user); + let pick = |ts: &TypeSet| -> Option { + match ts.fns.ids() { + [f] if !ts.fns.is_multi() => f.as_script(), + _ => None, + } + }; + self.accessor_add(class, name, pick(&g), pick(&s)); + ret + } + + /// Monotone accessor-table install. A conflicting re-install poisons + /// the entry (removed; never re-registered). New information re-fires + /// every same-name Read/Write constraint at its live contexts -- + /// registrations are rare (a handful per corpus), so the cons scan is + /// cheap. + fn accessor_add( + &mut self, + c: super::types::ClassId, + name: super::types::NameId, + getter: Option, + setter: Option, + ) { + if getter.is_none() && setter.is_none() { + return; + } + if self.accessor_poisoned.contains(&(c, name)) { + return; + } + let cur = self + .accessors + .get(&(c, name)) + .copied() + .unwrap_or((None, None)); + fn merge( + old: Option, + new: Option, + ) -> Result<(Option, bool), ()> { + match (old, new) { + (None, Some(n)) => Ok((Some(n), true)), + (Some(o), Some(n)) if o != n => Err(()), + (o, _) => Ok((o, false)), + } + } + let (Ok((g2, cg)), Ok((s2, cs))) = (merge(cur.0, getter), merge(cur.1, setter)) else { + self.accessors.remove(&(c, name)); + self.accessor_poisoned.insert((c, name)); + return; + }; + if !cg && !cs { + return; + } + self.accessors.insert((c, name), (g2, s2)); + for m in [getter, setter].into_iter().flatten() { + let this_cell = self.engine.cell(CellKey::This { + script: m, + ctx: CTX0, + }); + let ts = TypeSet { + obj: ObjType::ClassAny(c), + ..TypeSet::default() + }; + self.engine.raise(this_cell, &ts, (SEED, CTX0)); + } + for ci in 0..self.engine.cons.len() { + let hit = match &self.engine.cons[ci] { + Constraint::Read { name: n, .. } | Constraint::Write { name: n, .. } => *n == name, + _ => false, + }; + if hit { + let csid = self.engine.con_script[ci]; + for cx in self + .engine + .live_ctxs + .get(&csid) + .cloned() + .unwrap_or_default() + { + self.engine + .enqueue(super::engine::ConId(u32::try_from(ci).unwrap()), cx); + } + } + } + } + + /// Record scripted fn ids arriving at a callee's arg row (from the + /// Site's value, before the row join saturates), and forward them to + /// any tables this row is known to feed. + pub(super) fn note_arg_fn(&mut self, callee: ScriptId, arg: FormalIndex, v: &TypeSet) { + if v.fns.is_multi() || v.fns.ids().is_empty() { + return; + } + let mut fresh = false; + for &id in v.fns.ids() { + if !id.is_builtin() { + fresh |= self + .arg_fn_members + .entry((callee, arg)) + .or_default() + .insert(id); + } + } + if !fresh { + return; + } + for a in self + .arg_row_tables + .get(&(callee, arg)) + .cloned() + .unwrap_or_default() + { + let ids: Vec = v + .fns + .ids() + .iter() + .copied() + .filter(|&f| !f.is_builtin()) + .collect(); + self.add_table_members(a, &ids); + } + } + + /// Insert members into a table's list (capped, censused) and extend + /// the standing links if the table already has dispatch sites. + pub(super) fn add_table_members(&mut self, a: super::types::AbsId, ids: &[FnId]) { + let e = self.table_members.entry(a).or_default(); + for &f in ids { + if e.len() >= TABLE_MEMBER_CAP { + if !e.contains(&f) { + self.stats.table_members_capped += 1; + } + continue; + } + e.insert(f); + } + if let Some(c) = self.heap[a].class { + let ce = self.class_table_members.entry(c).or_default(); + for &f in ids { + if ce.len() >= TABLE_MEMBER_CAP { + break; + } + ce.insert(f); + } + } + self.install_table_links(a); + } + + /// Install join-row -> member-Arg links for members not yet linked + /// (no-op unless the table has dispatch sites and members grew). + pub(super) fn install_table_links(&mut self, a: super::types::AbsId) { + let Some(linked) = self.table_bound.get(&a).copied() else { + return; + }; + let members: Vec = self.table_members.get(&a).map_or_else(Vec::new, |m| { + let mut v: Vec = m.iter().copied().collect(); + v.sort_unstable(); + v + }); + if members.len() <= linked { + return; + } + for &f in &members { + let Some(f) = f.as_script() else { + continue; + }; + for i in 0..MAX_TRACKED_FORMALS { + let arg = FormalIndex::new(i); + let j = self.engine.cell(CellKey::TableArgJoin { abs: a, arg }); + let dst = self.engine.cell(CellKey::Arg { + script: f, + arg, + ctx: CTX0, + }); + self.engine.link(j, dst); + } + } + self.table_bound.insert(a, members.len()); + } + + /// An executed-but-unresolved call result: raise the unknown evidence + /// bit into the return destination (see `TypeSet::unknown`). + fn raise_unknown_ret( + &mut self, + script: ScriptId, + ctx: CtxId, + ret: super::engine::CKey, + user: (ConId, CtxId), + ) { + let ret_dst = self.engine.resolve(script, ctx, ret); + self.engine + .raise(ret_dst, &TypeSet::unknown_evidence(), user); + } + + /// Enter callee `f` from `(script, pc)` at `ctx`: mint/reuse the context, + /// count the budget, instantiate the callee's rows there. + fn enter(&mut self, ctx: CtxId, script: ScriptId, pc: Pc, f: ScriptId, poly: bool) -> CtxId { + let cx = if poly { + self.stats.call_ctx_degraded_polymorphic += 1; + CTX0 + } else { + self.ctxs + .push(ctx, Site::new(script, pc), f, &mut self.stats) + }; + if self.engine.instantiate(f, cx) && cx != CTX0 { + self.stats.ctxs_spent += 1; + } + cx + } + + /// The this-assertion filter: never bind a worse-than-asserted receiver + /// (AnyObject) into a homed method -- the polymorphic-dispatch site's + /// lost receiver must not destroy the body's asserted precision. + /// Precise receivers bind normally. + fn bind_this_ok(&self, f: ScriptId, v: &TypeSet) -> bool { + !(self.this_pin.contains_key(&f) && matches!(v.obj, ObjType::AnyObject | ObjType::AnyOf(_))) + } + + /// Escape: function values reaching an untracked sink get Any joined + /// into their generic-context args and this, once. + pub(super) fn do_escape(&mut self, v: &TypeSet, user: (ConId, CtxId)) { + for &f in v.fns.ids().to_vec().iter() { + let Some(script) = f.as_script() else { + continue; + }; + if !self.escaped.insert(f) { + continue; + } + if self.escape_log.len() < 20 { + self.escape_log.push((f, user.0)); + } + // Escaped bindings are executed-but-unresolved, not "every value + // was seen": fabricated definite prim bits (TypeSet::any()) + // poisoned every field cell an escaped method writes through + // `this`, indistinguishable from real string/bool evidence. + let any = TypeSet::unresolved(); + for i in 0..MAX_TRACKED_FORMALS { + let c = self.engine.cell(CellKey::Arg { + script, + arg: FormalIndex::new(i), + ctx: CTX0, + }); + self.engine.raise(c, &any, (SEED, CTX0)); + } + let c = self.engine.cell(CellKey::This { script, ctx: CTX0 }); + self.engine.raise(c, &any, (SEED, CTX0)); + } + } + + fn note_site_calls(&mut self, script: ScriptId, pc: Pc, cts: &TypeSet) { + // Output-table join: sink drops are diagnostics-only anyway. + let mut scratch = Vec::new(); + let scripted = cts.fns.scripted_only(&mut scratch); + let site = Site::new(script, pc); + let e = self.site_calls.entry(site).or_default(); + e.join_from(&scripted, &mut scratch); + if !(cts.fns.is_multi() && cts.unknown) { + let e = self.site_likely_calls.entry(site).or_default(); + e.join_from(&scripted, &mut scratch); + } + } + + fn note_site_native(&mut self, script: ScriptId, pc: Pc, fns: &BoundedFnSet) { + if fns.is_empty() && !fns.is_multi() { + return; + } + let verdict = match fns.ids() { + [f] if !fns.is_multi() && f.native_index().is_some() => Some(*f), + _ => None, + }; + let e = self.site_native.entry(Site::new(script, pc)).or_default(); + match verdict { + Some(f) => e.observe(f), + None => *e = super::types::Agreed::Conflict, + } + } + + fn note_site_ctor_native(&mut self, script: ScriptId, pc: Pc, fns: &BoundedFnSet) { + if fns.is_empty() && !fns.is_multi() { + return; + } + let verdict = match fns.ids() { + [f] if !fns.is_multi() && f.native_index().is_some() => Some(*f), + _ => None, + }; + let e = self + .site_ctor_native + .entry(Site::new(script, pc)) + .or_default(); + match verdict { + Some(f) => e.observe(f), + None => *e = super::types::Agreed::Conflict, + } + } + + fn note_site_apply( + &mut self, + script: ScriptId, + ctx: CtxId, + pc: Pc, + fns: &BoundedFnSet, + form: CallForm, + ) { + // Output-table join: sink drops are diagnostics-only anyway. + let mut scratch = Vec::new(); + let scripted = fns.scripted_only(&mut scratch); + if ctx != CTX0 { + self.site_apply_ctx + .entry((ctx, Site::new(script, pc))) + .or_default() + .join_from(&scripted, &mut scratch); + } + let e = self + .site_apply + .entry(Site::new(script, pc)) + .or_insert((BoundedFnSet::default(), form)); + e.0.join_from(&scripted, &mut scratch); + if fns.is_empty() && !fns.is_multi() { + return; + } + let native = match fns.ids() { + [f] if !fns.is_multi() && f.native_index().is_some() => Some(*f), + _ => None, + }; + let e = self + .site_apply_native + .entry(Site::new(script, pc)) + .or_default(); + match native { + Some(f) => e.observe(f), + None => *e = super::types::Agreed::Conflict, + } + } +} + +pub type Escaped = HashSet; diff --git a/js/src/night/compiler/src/likelier/dump.rs b/js/src/night/compiler/src/likelier/dump.rs new file mode 100644 index 0000000000000..e6f49d7a2cd14 --- /dev/null +++ b/js/src/night/compiler/src/likelier/dump.rs @@ -0,0 +1,284 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Canonical `LikelyFacts` dump (`Diagnostics::facts`), written at the +//! `layout_env` seam so both analyses produce the identical format. The +//! parity diff script (`tools/diff-facts.py`) compares two dumps per table +//! per site. + +use crate::facts::LikelyFacts; +use crate::ids::{JsString, NameId, Names}; +use std::fmt::Write as _; + +/// Property names are UTF-16; escape anything outside the identifier-ish +/// range so lines stay unambiguous and single-line. +fn esc(name: &JsString) -> String { + let mut s = String::new(); + for &u in name.chars() { + match char::from_u32(u32::from(u)) { + Some(c) if c.is_ascii_alphanumeric() || c == '_' || c == '$' || c == '.' => s.push(c), + _ => { + let _ = write!(s, "%{u:04x}"); + } + } + } + if s.is_empty() { + s.push_str("%empty"); + } + s +} + +fn names(tbl: &Names, list: &[NameId]) -> String { + list.iter() + .map(|&n| esc(tbl.get(n))) + .collect::>() + .join(",") +} + +pub fn dump_facts(facts: &LikelyFacts, path: &str) { + let mut out = String::new(); + let _ = writeln!(out, "# night-facts-dump v1"); + let _ = writeln!(out, "meta n_classes = {}", facts.n_classes); + let _ = writeln!(out, "meta n_cons = {}", facts.n_cons); + + let mut lines: Vec = Vec::new(); + for (site, targets) in facts.scripted_call_sites() { + let mut t = targets.to_vec(); + t.sort_unstable(); + let t: Vec = t.iter().map(|x| x.to_string()).collect(); + lines.push(format!("calls {site} = {}", t.join(","))); + } + for (&site, &(target, kind)) in &facts.accessor_sites { + lines.push(format!("accessor_sites {site} = {target} {kind}")); + } + for (&site, &kind) in &facts.apply_sites { + lines.push(format!("apply_sites {site} = {kind:?}")); + } + for (&site, &target) in &facts.apply_targets { + lines.push(format!("apply_targets {site} = {target}")); + } + for (&(entry, site), &target) in &facts.apply_targets_in { + lines.push(format!("apply_targets_in {entry} {site} = {target}")); + } + for (&site, targets) in &facts.apply_target_sets { + let t: Vec = targets.iter().map(|t| t.to_string()).collect(); + lines.push(format!("apply_target_sets {site} = [{}]", t.join(" "))); + } + for name in &facts.accessor_names { + lines.push(format!( + "accessor_names {} = 1", + esc(facts.names.get(*name)) + )); + } + for (&(script, i), &claim) in &facts.arg_types { + lines.push(format!("arg_types {script}:{i} = {:#x}", claim.bits())); + } + for (&name, &claim) in &facts.gname_types { + lines.push(format!( + "gname_types {} = {:#x}", + esc(facts.names.get(name)), + claim.bits() + )); + } + for (&site, &claim) in &facts.call_types { + lines.push(format!("call_types {site} = {:#x}", claim.bits())); + } + for (&site, &claim) in &facts.aliased_sites { + lines.push(format!("aliased_sites {site} = {:#x}", claim.bits())); + } + for &site in &facts.fractional_arith_sites { + lines.push(format!("fractional_arith_sites {site}")); + } + for &site in &facts.string_arith_sites { + lines.push(format!("string_arith_sites {site}")); + } + for (&site, key) in &facts.lit_stamps { + lines.push(format!("lit_stamps {site} = {}", key.get())); + } + for (&root, &(prims, range)) in &facts.array_elem_claims { + lines.push(format!( + "array_elem_claims {root} = {:#x} {} {}", + prims.bits(), + range.lo, + range.hi + )); + } + for (&site, &root) in &facts.array_alloc_sites { + lines.push(format!("array_alloc_sites {site} = {root}")); + } + for (&site, &root) in &facts.array_elem_recv { + lines.push(format!("array_elem_recv {site} = {root}")); + } + for (&site, &key) in &facts.construct_site_keys { + lines.push(format!("construct_site_keys {site} = {key}")); + } + for (&site, r) in &facts.call_sites { + if matches!(r, crate::facts::CallResolution::Native) { + lines.push(format!("native_calls {site} = 1")); + } + } + for (&s, &(lo, hi)) in &facts.this_layouts { + lines.push(format!("this_layouts {s} = {lo} {hi}")); + } + for (&ctor, class) in &facts.classes { + let row: Vec = class.fields.iter().map(|f| f.name).collect(); + lines.push(format!( + "class_layouts {ctor} = {}", + names(&facts.names, &row) + )); + let m: Vec = class + .fields + .iter() + .map(|f| format!("{:#x}", f.prims.bits())) + .collect(); + lines.push(format!("class_layout_masks {ctor} = {}", m.join(","))); + if class.fields.iter().any(|f| f.typed_prims != f.prims) { + let m: Vec = class + .fields + .iter() + .map(|f| format!("{:#x}", f.typed_prims.bits())) + .collect(); + lines.push(format!("class_layout_typed_masks {ctor} = {}", m.join(","))); + } + } + for (&site, &(lo, hi, slot, mask)) in &facts.prop_sites { + lines.push(format!( + "prop_sites {site} = {lo} {hi} {slot} {:#x}", + mask.bits() + )); + } + for (&site, &mask) in &facts.elem_sites { + lines.push(format!("elem_sites {site} = {:#x}", mask.bits())); + } + for (&site, &mask) in &facts.elem_write_sites { + lines.push(format!("elem_write_sites {site} = {:#x}", mask.bits())); + } + for (&site, &kind) in &facts.ta_elem_sites { + lines.push(format!("ta_elem_sites {site} = {}", kind.code())); + } + for &site in &facts.elem_poly_sites { + lines.push(format!("elem_poly_sites {site} = 1")); + } + for (&site, &(lo, hi)) in &facts.field_cls_sites { + lines.push(format!("field_cls_sites {site} = {lo} {hi}")); + } + for (&(sid, i), &(lo, hi)) in &facts.arg_cls { + lines.push(format!("arg_cls {sid}:{} = {lo} {hi}", i.get())); + } + for (&site, &mask) in &facts.field_sites { + lines.push(format!("field_sites {site} = {:#x}", mask.bits())); + } + for (&site, &(lo, hi, mask)) in &facts.typed_sites { + lines.push(format!("typed_sites {site} = {lo} {hi} {:#x}", mask.bits())); + } + for &s in &facts.deleg_inits { + lines.push(format!("deleg_inits {s} = 1")); + } + for (&s, &key) in &facts.deleg_restamps { + lines.push(format!("deleg_restamps {s} = {key}")); + if let Some(class) = facts.classes.get(&key) { + let row: Vec = class.fields.iter().map(|f| f.name).collect(); + lines.push(format!( + "deleg_restamp_layouts {s} = {}", + names(&facts.names, &row) + )); + } + } + for (&s, &key) in &facts.ctor_stamps { + lines.push(format!("ctor_stamps {s} = {key}")); + } + for (&s, &n) in &facts.ctor_nslots { + lines.push(format!("ctor_nslots {s} = {n}")); + } + for (&lo, (fields, masks)) in &facts.group_tables { + let m: Vec = masks.iter().map(|x| format!("{:#x}", x.bits())).collect(); + lines.push(format!( + "group_tables {lo} = {} / {}", + names(&facts.names, fields), + m.join(",") + )); + } + // Derived, stably-keyed views for cross-analysis comparison: the dense + // layout keys above are a per-analysis numbering artifact. + for (&f, &key) in &facts.ctor_stamps { + if let Some(class) = facts.classes.get(&key) { + let row: Vec = class.fields.iter().map(|f| f.name).collect(); + lines.push(format!("ctor_layouts {f} = {}", names(&facts.names, &row))); + let m: Vec = class + .fields + .iter() + .map(|f| format!("{:#x}", f.prims.bits())) + .collect(); + lines.push(format!("ctor_layout_masks {f} = {}", m.join(","))); + } + } + for (&m, &(lo, hi)) in &facts.this_layouts { + let row: Option> = if lo == hi { + facts + .classes + .get(&lo) + .map(|c| c.fields.iter().map(|f| f.name).collect()) + } else { + facts.group_tables.get(&lo).map(|(t, _)| t.clone()) + }; + if let Some(row) = row { + let kind = if lo == hi { "exact" } else { "range" }; + lines.push(format!( + "this_slots {m} = {kind} {}", + names(&facts.names, &row) + )); + } + } + for (&site, native) in &facts.apply_natives { + lines.push(format!("apply_natives {site} = {native:?}")); + } + for (&site, &(local, key)) in &facts.local_restamps { + lines.push(format!("local_restamp {site} = l{local} -> {key}")); + } + for (&sid, &(formal, key)) in &facts.arg_restamps { + lines.push(format!( + "arg_restamp {} = a{} -> {}", + sid.get(), + formal, + key.get() + )); + } + for (&site, &(lo, hi, slot, mask)) in &facts.prop_sites { + let kind = if lo == hi { "exact" } else { "range" }; + lines.push(format!( + "prop_slots {site} = {kind} {}..{} {slot} {:#x}", + lo.get(), + hi.get(), + mask.bits() + )); + } + for (&sid, e) in &facts.script_effects { + let mut fields = String::new(); + for &(range, name) in &e.field_writes { + let cls = match range { + Some((lo, hi)) => format!("{}..{}", lo.get(), hi.get()), + None => "?".to_string(), + }; + let _ = write!(fields, " {}:{}", cls, esc(facts.names.get(name))); + } + lines.push(format!( + "effect sid#{} = {}{fields}{}", + sid.get(), + e.label(), + if e.gname_writes.is_empty() { + String::new() + } else { + format!(" gnames={}", names(&facts.names, &e.gname_writes)) + }, + )); + } + lines.sort(); + for l in lines { + let _ = writeln!(out, "{l}"); + } + match std::fs::write(path, &out) { + Ok(()) => crate::diag_line!("likelier: facts dump written to {path}"), + Err(e) => log::error!("likelier: facts dump to {path} failed: {e}"), + } +} diff --git a/js/src/night/compiler/src/likelier/effects.rs b/js/src/night/compiler/src/likelier/effects.rs new file mode 100644 index 0000000000000..079feeeb3b1e8 --- /dev/null +++ b/js/src/night/compiler/src/likelier/effects.rs @@ -0,0 +1,601 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Post-fixpoint per-script effect summaries: a per-script op walk +//! classifying every op against a known-benign set, a call-edge fold over +//! the solved resolution tables, and a transitive fixpoint that fills +//! `LikelyFacts::script_effects`. Produced from the solved state only -- +//! never fed back into the solve. + +use super::builtins::{self, NativeEffect}; +use super::emit::LayoutPlan; +use super::types::{Agreed, ClassId, NameId, Names}; +use super::Solver; +use crate::bytecode::{JSOp, OpcodeVisitor, Script}; +use crate::facts::{CallResolution, EffectSummary, LikelyFacts}; +use crate::ids::{LayoutKey, Pc, ScriptId, Site}; +use crate::source::{Source, SourceObject}; +use rustc_hash::FxHashMap as HashMap; +use rustc_hash::FxHashSet as HashSet; + +const FIELD_CAP: usize = 16; +const GNAME_CAP: usize = 8; + +/// A field write's receiver, pre-plan-mapping. +#[derive(Clone, Copy, PartialEq, Eq)] +enum FieldRecv { + Cls(ClassId), + /// The write's receiver is the script's own `this` (no agreed class at + /// the site): mapped through `this_layouts`. + This, + Unknown, +} + +/// The op that pushed a value, for the unresolved-callee labels: a linear +/// per-block guess (joins keep the fall-through path), diagnostic-only. +#[derive(Clone, Copy, Default)] +struct Origin { + op: Option, + name: Option, + num: Option, +} + +#[derive(Default)] +struct LocalEffects { + sum: EffectSummary, + field_writes: Vec<(FieldRecv, NameId)>, + /// Call and construct sites whose targets fold in transitively. + calls: Vec, + /// Property reads of a registered accessor name: fold the resolved + /// getter, saturate when unresolved. + accessor_reads: Vec, + /// Per call pc, what pushed the callee. + callee_origin: HashMap, +} + +struct EffectWalk<'a> { + sid: ScriptId, + script: &'a Script, + source: &'a Source, + names: &'a mut Names, + recv_class: &'a HashMap>, + this_writes: &'a HashSet, + accessor_names: &'a HashSet, + cur_pc: Pc, + stack: Vec, + out: LocalEffects, +} + +impl<'a> EffectWalk<'a> { + fn sim_stack(&mut self, pc: Pc, op: JSOp, nuses: usize, ndefs: usize) { + use JSOp::*; + match op { + Dup => { + let t = self.stack.last().copied().unwrap_or_default(); + self.stack.push(t); + } + Dup2 => { + let n = self.stack.len(); + let a = self + .stack + .get(n.wrapping_sub(2)) + .copied() + .unwrap_or_default(); + let b = self.stack.last().copied().unwrap_or_default(); + self.stack.push(a); + self.stack.push(b); + } + Swap => { + let n = self.stack.len(); + if n >= 2 { + self.stack.swap(n - 1, n - 2); + } + } + // Operand-carrying shufflers: net effect applied in the + // per-op methods below. + DupAt => self.stack.push(Origin::default()), + Pick | Unpick => {} + Call | CallContent | CallIgnoresRv | New | NewContent => { + let n = self.stack.len(); + let callee = n + .checked_sub(nuses) + .and_then(|i| self.stack.get(i)) + .copied() + .unwrap_or_default(); + self.out.callee_origin.insert(pc, callee); + self.pop_push(op, nuses, ndefs); + } + _ => self.pop_push(op, nuses, ndefs), + } + } + + fn pop_push(&mut self, op: JSOp, nuses: usize, ndefs: usize) { + let keep = self.stack.len().saturating_sub(nuses); + self.stack.truncate(keep); + for _ in 0..ndefs { + self.stack.push(Origin { + op: Some(op), + name: None, + num: None, + }); + } + } + + fn patch_top_name(&mut self, name_index: u32) { + let name = self.atom_id(name_index); + if let Some(t) = self.stack.last_mut() { + t.name = name; + } + } + + fn patch_top_num(&mut self, n: u32) { + if let Some(t) = self.stack.last_mut() { + t.num = Some(n); + } + } + fn atom_id(&mut self, index: u32) -> Option { + let gc = *self.script.gcthings.get(index as usize)?; + if gc.is_other() { + return None; + } + match self.source.object(gc) { + SourceObject::String(s) => Some(self.names.intern(s.chars())), + _ => None, + } + } + + fn prop_write(&mut self, name_index: u32) { + if self.out.sum.top { + return; + } + let Some(name) = self.atom_id(name_index) else { + self.out.sum.saturate("prop-name"); + return; + }; + let site = Site::new(self.sid, self.cur_pc); + let cls = match self.recv_class.get(&site).and_then(|a| a.get().copied()) { + Some(c) => FieldRecv::Cls(c), + None if self.this_writes.contains(&site) => FieldRecv::This, + None => FieldRecv::Unknown, + }; + if !self.out.field_writes.contains(&(cls, name)) { + if self.out.field_writes.len() >= FIELD_CAP { + self.out.sum.saturate("field-cap"); + return; + } + self.out.field_writes.push((cls, name)); + } + } + + fn gname_write(&mut self, name_index: u32) { + if self.out.sum.top { + return; + } + let Some(name) = self.atom_id(name_index) else { + self.out.sum.saturate("gname-name"); + return; + }; + if !self.out.sum.gname_writes.contains(&name) { + if self.out.sum.gname_writes.len() >= GNAME_CAP { + self.out.sum.saturate("gname-cap"); + return; + } + self.out.sum.gname_writes.push(name); + } + } +} + +impl<'a> OpcodeVisitor for EffectWalk<'a> { + fn before_op(&mut self, pc: Pc, op: JSOp, nuses: usize, ndefs: usize) { + self.cur_pc = pc; + self.sim_stack(pc, op, nuses, ndefs); + if self.out.sum.top { + return; + } + use JSOp::*; + match op { + // Classified by the operand-carrying methods below. + SetProp | StrictSetProp | InitProp | InitHiddenProp | InitLockedProp | SetGName + | StrictSetGName | InitGLexical | GetProp => {} + // Element writes. The Init* forms hit the fresh literal under + // construction, folded in conservatively. + SetElem | StrictSetElem | InitElem | InitHiddenElem | InitLockedElem + | InitElemArray | InitElemInc => self.out.sum.elems_write = true, + SetAliasedVar => self.out.sum.env_write = true, + Call | CallContent | CallIgnoresRv | New | NewContent => { + self.out.calls.push(Site::new(self.sid, pc)); + } + // The known-benign set: literals, stack shuffling, frame-local + // reads/writes, arithmetic and comparison, control flow, fresh + // allocations, and heap reads. Coercions and reads can reach + // user code on exotic receivers, which is why consumers keep + // only guarded/recoverable state on a summary's strength. + Undefined | Null | False | True | Int32 | Zero | One | Int8 | Uint16 | Uint24 + | Double | String | Symbol | Void | Typeof | TypeofExpr | TypeofEq | Pos | Neg + | BitNot | Not | BitOr | BitXor | BitAnd | Eq | Ne | StrictEq | StrictNe + | StrictConstantEq | StrictConstantNe | Lt | Gt | Le | Ge | Instanceof | In | Lsh + | Rsh | Ursh | Add | Sub | Inc | Dec | Mul | Div | Mod | Pow | NopIsAssignOp + | ToPropertyKey | ToNumeric | ToString | IsNullOrUndefined | GlobalThis | GetElem + | HasOwn | CheckIsObj | CheckObjCoercible | JumpTarget | LoopHead | Goto + | JumpIfFalse | JumpIfTrue | And | Or | Coalesce | Case | Default | TableSwitch + | Return | GetRval | SetRval | RetRval | CheckReturn | Throw | ThrowMsg + | Uninitialized | InitLexical | CheckLexical | CheckAliasedLexical | CheckThis + | GetGName | GetArg | GetFrameArg | GetLocal | ArgumentsLength | GetActualArg + | GetAliasedVar | GetIntrinsic | Callee | SetArg | SetLocal | FunctionThis | Pop + | PopN | Dup | Dup2 | DupAt | Swap | Pick | Unpick | Nop | Lineno + | NopDestructuring | NewInit | NewObject | NewArray | Arguments | IsConstructing + | Try | Exception | Finally | Lambda | DebugLeaveLexicalEnv | BindUnqualifiedGName => {} + _ => self.out.sum.saturate(format!("{op:?}")), + } + } + + fn set_prop(&mut self, name_index: u32) { + self.prop_write(name_index); + } + fn strict_set_prop(&mut self, name_index: u32) { + self.prop_write(name_index); + } + fn init_prop(&mut self, name_index: u32) { + self.prop_write(name_index); + } + fn init_hidden_prop(&mut self, name_index: u32) { + self.prop_write(name_index); + } + fn init_locked_prop(&mut self, name_index: u32) { + self.prop_write(name_index); + } + fn set_g_name(&mut self, name_index: u32) { + self.gname_write(name_index); + } + fn strict_set_g_name(&mut self, name_index: u32) { + self.gname_write(name_index); + } + fn init_g_lexical(&mut self, name_index: u32) { + self.gname_write(name_index); + } + + fn get_prop(&mut self, name_index: u32) { + self.patch_top_name(name_index); + if self.out.sum.top { + return; + } + let pc = self.cur_pc; + if let Some(name) = self.atom_id(name_index) { + if self.accessor_names.contains(&name) { + self.out.accessor_reads.push(Site::new(self.sid, pc)); + } + } + } + + fn get_g_name(&mut self, name_index: u32) { + self.patch_top_name(name_index); + } + fn get_intrinsic(&mut self, name_index: u32) { + self.patch_top_name(name_index); + } + fn get_arg(&mut self, argno: u16) { + self.patch_top_num(u32::from(argno)); + } + fn get_frame_arg(&mut self, argno: u16) { + self.patch_top_num(u32::from(argno)); + } + fn get_local(&mut self, localno: u32) { + self.patch_top_num(localno); + } + fn get_aliased_var(&mut self, _hops: u16, slot: u32) { + self.patch_top_num(slot); + } + + fn dup_at(&mut self, n: u32) { + let len = self.stack.len(); + let src = len + .checked_sub(2 + n as usize) + .and_then(|i| self.stack.get(i)) + .copied() + .unwrap_or_default(); + if let Some(t) = self.stack.last_mut() { + *t = src; + } + } + fn pick(&mut self, n: u8) { + let len = self.stack.len(); + if let Some(i) = len.checked_sub(1 + usize::from(n)) { + let v = self.stack.remove(i); + self.stack.push(v); + } + } + fn unpick(&mut self, n: u8) { + let len = self.stack.len(); + if len == 0 { + return; + } + if let Some(i) = len.checked_sub(1 + usize::from(n)) { + let v = self.stack.pop().unwrap(); + self.stack.insert(i.min(self.stack.len()), v); + } + } +} + +fn origin_str(names: &Names, o: Origin) -> String { + use std::fmt::Write as _; + let Some(op) = o.op else { + return "?".to_string(); + }; + let mut s = format!("{op:?}"); + if let Some(n) = o.name { + let _ = write!(s, ":{}", String::from_utf16_lossy(names.get(n).chars())); + } else if let Some(i) = o.num { + let _ = write!(s, ":{i}"); + } + s +} + +fn native_summary(effect: NativeEffect, sum: &mut EffectSummary) { + match effect { + NativeEffect::Pure => {} + NativeEffect::Elems => sum.elems_write = true, + NativeEffect::Top => sum.saturate("native"), + } +} + +/// Fold one resolved callable into (deps, sum): scripts become fixpoint +/// dependencies, natives classify by table, the Array/typed-array ctors +/// are allocation-only. +fn fold_fn( + sv: &Solver<'_>, + f: super::types::FnId, + deps: &mut Vec, + sum: &mut EffectSummary, +) { + if let Some(s) = f.as_script() { + if !deps.contains(&s) { + deps.push(s); + } + } else if let Some(info) = sv.natives.get(f) { + let name = sv.names.get(info.name); + native_summary( + builtins::native_effect(info.kind, name.chars(), info.result.is_some()), + sum, + ); + } else if f == super::types::FnId::ARRAY_CTOR || f.typed_array_kind().is_some() { + // Allocation-only. + } else { + sum.saturate("callable"); + } +} + +pub(super) fn emit_effect_summaries( + sv: &mut Solver<'_>, + facts: &mut LikelyFacts, + plan: &LayoutPlan, +) { + // Sites whose Write constraint names the script's own `this` as + // receiver, for the `this_layouts` fallback when the site carries no + // agreed class. + let mut this_writes: HashSet = HashSet::default(); + for (ci, con) in sv.engine.cons.iter().enumerate() { + if let super::engine::Constraint::Write { recv, pc, .. } = con { + if matches!(recv, super::engine::CKey::This) { + this_writes.insert(Site::new(sv.engine.con_script[ci], *pc)); + } + } + } + // Phase A: per-script local walks. + let mut sids: Vec = sv + .source + .objects() + .filter_map(|(oid, obj)| match obj { + SourceObject::Script(_) => Some(ScriptId::new(oid.id())), + _ => None, + }) + .collect(); + sids.sort(); + let mut locals: HashMap = HashMap::default(); + for &sid in &sids { + let SourceObject::Script(script) = sv.source.object(sid.source()) else { + continue; + }; + let mut walk = EffectWalk { + sid, + script, + source: sv.source, + names: &mut sv.names, + recv_class: &sv.site_recv_class, + this_writes: &this_writes, + accessor_names: &facts.accessor_names, + cur_pc: Pc::new(0), + stack: Vec::new(), + out: LocalEffects::default(), + }; + if script.is_generator_or_async { + walk.out.sum.saturate("generator"); + } else { + walk = script.parser().visit(walk); + } + locals.insert(sid, walk.out); + } + + // Call ops the scan lowered to non-Call constraints: elem-builtin + // forms (`a.push(v)`) write elements; alloc forms (`new Array(n)`, + // typed-array ctors) are allocation-only. Neither enters the call + // census, so without these sets they mislabel as uncensused. + let mut elem_sites: HashSet = HashSet::default(); + let mut alloc_call_sites: HashSet = HashSet::default(); + for (ci, con) in sv.engine.cons.iter().enumerate() { + match con { + super::engine::Constraint::ElemBuiltin { pc, .. } => { + elem_sites.insert(Site::new(sv.engine.con_script[ci], *pc)); + } + super::engine::Constraint::Alloc { pc, .. } => { + alloc_call_sites.insert(Site::new(sv.engine.con_script[ci], *pc)); + } + _ => {} + } + } + + // Phase B: map agreed write-site classes to planned key ranges, and + // resolve every call edge once. Unresolvable edges saturate here. + let mut deps: HashMap> = HashMap::default(); + for &sid in &sids { + let local = locals.get_mut(&sid).unwrap(); + let mut sum = std::mem::take(&mut local.sum); + for &(recv, name) in &local.field_writes { + let range = match recv { + FieldRecv::Cls(c) => plan + .range_of(sv.group_of_class(c)) + .map(|(lo, hi)| (LayoutKey::new(lo), LayoutKey::new(hi))), + FieldRecv::This => facts.this_layouts.get(&sid).copied(), + FieldRecv::Unknown => None, + }; + sum.field_writes.push((range, name)); + } + let mut dep_list: Vec = Vec::new(); + for &site in &local.calls { + if sum.top { + break; + } + // An empty non-multi scripted set says the eval saw no + // callables -- fall through to the native/apply tables rather + // than saturating on it (a native callee often leaves an empty + // scripted record beside its `site_native` answer). A multi + // set saturates regardless (its ids are dropped on collapse). + let scripted = sv + .site_calls + .get(&site) + .filter(|f| f.is_multi() || !f.ids().is_empty()); + if let Some(fns) = scripted { + if fns.is_multi() { + sum.saturate(format!("multi@{}", site.pc)); + } else { + for &f in fns.ids() { + fold_fn(sv, f, &mut dep_list, &mut sum); + } + } + } else if let Some(&f) = sv.site_native.get(&site).and_then(Agreed::get) { + fold_fn(sv, f, &mut dep_list, &mut sum); + } else if let Some((fns, _)) = sv.site_apply.get(&site) { + if fns.is_multi() || fns.ids().is_empty() { + sum.saturate(format!("multi-apply@{}", site.pc)); + } else { + for &f in fns.ids() { + fold_fn(sv, f, &mut dep_list, &mut sum); + } + } + } else if let Some(&f) = sv.site_ctor_native.get(&site).and_then(Agreed::get) { + fold_fn(sv, f, &mut dep_list, &mut sum); + } else if elem_sites.contains(&site) { + // The receiver resolved to nothing callable in the census: + // the element-node effect below is the whole story. + } else if alloc_call_sites.contains(&site) { + // Allocation-only. + } else if sv.site_calls.contains_key(&site) || sv.site_native.contains_key(&site) { + let o = local + .callee_origin + .get(&site.pc) + .copied() + .unwrap_or_default(); + sum.saturate(format!("empty@{}({})", site.pc, origin_str(&sv.names, o))); + } else { + let o = local + .callee_origin + .get(&site.pc) + .copied() + .unwrap_or_default(); + sum.saturate(format!( + "uncensused@{}({})", + site.pc, + origin_str(&sv.names, o) + )); + } + if elem_sites.contains(&site) { + sum.elems_write = true; + } + } + for &site in &local.accessor_reads { + if sum.top { + break; + } + match facts.call_sites.get(&site) { + Some(CallResolution::Scripted(targets)) if !targets.is_empty() => { + for &t in targets { + if !dep_list.contains(&t) { + dep_list.push(t); + } + } + } + _ => sum.saturate("accessor"), + } + } + local.sum = sum; + deps.insert(sid, dep_list); + } + + // Phase C: transitive fixpoint. Monotone joins over a capped lattice. + let mut rev: HashMap> = HashMap::default(); + for &sid in &sids { + for &d in &deps[&sid] { + rev.entry(d).or_default().push(sid); + } + } + let mut summaries: HashMap = sids + .iter() + .map(|&sid| (sid, locals[&sid].sum.clone())) + .collect(); + let mut work: Vec = sids.clone(); + let mut queued: HashSet = sids.iter().copied().collect(); + while let Some(sid) = work.pop() { + queued.remove(&sid); + let mut joined = locals[&sid].sum.clone(); + for &d in &deps[&sid] { + if joined.top { + break; + } + let Some(ds) = summaries.get(&d) else { + // A dep outside the walked source (should not happen): + // nothing is known about it. + joined.saturate("dep-missing"); + break; + }; + join_into(&mut joined, ds); + } + if summaries[&sid] != joined { + summaries.insert(sid, joined); + if let Some(callers) = rev.get(&sid) { + for &c in callers { + if queued.insert(c) { + work.push(c); + } + } + } + } + } + facts.script_effects = summaries; +} + +fn join_into(dst: &mut EffectSummary, src: &EffectSummary) { + if src.top { + dst.saturate(format!("dep:{}", src.top_why.as_deref().unwrap_or("?"))); + return; + } + for fw in &src.field_writes { + if !dst.field_writes.contains(fw) { + if dst.field_writes.len() >= FIELD_CAP { + dst.saturate("field-cap"); + return; + } + dst.field_writes.push(*fw); + } + } + for g in &src.gname_writes { + if !dst.gname_writes.contains(g) { + if dst.gname_writes.len() >= GNAME_CAP { + dst.saturate("gname-cap"); + return; + } + dst.gname_writes.push(*g); + } + } + dst.elems_write |= src.elems_write; + dst.env_write |= src.env_write; +} diff --git a/js/src/night/compiler/src/likelier/emit.rs b/js/src/night/compiler/src/likelier/emit.rs new file mode 100644 index 0000000000000..244732c203ba6 --- /dev/null +++ b/js/src/night/compiler/src/likelier/emit.rs @@ -0,0 +1,2919 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `LikelyFacts` production: the solved cell graph projected onto the +//! translator-facing tables. +//! +//! The facts fall into four families, and each has its own emission path +//! below. They are produced in this order because each depends on the one +//! before it: +//! +//! 1. **Value claims** -- what a value at a program point is likely to be. +//! A script's receiver and formals, a call's result, a closure-slot +//! read: each is its cell joined over the contexts the script was live +//! at, projected through the claim tiers. Produced by +//! [`Solver::emit_value_claims`]. +//! +//! 2. **Call-site resolution** -- how each call site resolved, as one +//! [`CallResolution`]: a modeled native with an inline arm, or a small +//! set of scripted targets to guard on. Produced by +//! [`Solver::emit_call_sites`], which also resolves the `.call`/`.apply` +//! delegation targets the layout analysis walks through. +//! +//! 3. **The predicted heap** -- classes, the regions they meet in, the +//! field order each class's instances get, and the value claim on each +//! field. This is the constructor-body analysis, and it lives in +//! [`LayoutPlan`]: it expands each constructor's `this` writes into an +//! ordered layout row, assigns the dense keys that let one range guard +//! cover a whole predictor group, and folds the group's members into the +//! universal prefix table a range fact reads. +//! +//! 4. **Per-site heap facts** -- for each property and element site, the +//! key range, slot and claim it may guard on, resolved against the plan. +//! Produced by [`Solver::emit_site_facts`] and the class rows and array +//! claims that follow it. + +use super::heap::ClassKey; +use super::scan::TEvent; +use super::stats::CapDrops; +use super::types::{observe, Agreed, AgreedSet}; +use super::types::{ClassId, NameId, ObjType, TypeSet}; +use super::{SharedCtorSite, Solver}; +use crate::constants::{LAY_CAP, MAX_DELEG_DEPTH, MAX_SITE_TARGETS, MAX_TRACKED_FORMALS}; +use crate::facts::LikelyFacts; +use crate::facts::{CallResolution, Claim, ClassFacts, ClassFieldFacts, ValueRange}; +use crate::ids::{ + ArgIndex, FormalIndex, LayoutKey, Pc, RegionRoot, ScriptId, Site, SlotIndex, VarId, +}; +use crate::opsem::{Prims, PRIM_DOUBLE, PRIM_INT32, PRIM_NULL, PRIM_UNDEFINED}; +use rustc_hash::FxHashMap as HashMap; +use rustc_hash::FxHashSet as HashSet; +/// cover. A class that has a constructor groups with every other class of +/// that constructor; a class that does not (an allocation-site class, or +/// one of a shared-generated constructor's per-prototype classes) is its +/// own group. +/// +/// The two cases are packed into one integer so the whole key assignment +/// can sort them together; the tag bit keeps the two id spaces from +/// colliding. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub(super) struct GroupId(u64); + +impl GroupId { + const CTOR_TAG: u64 = 1 << 32; + + fn of_ctor(f: ScriptId) -> GroupId { + GroupId(GroupId::CTOR_TAG | u64::from(f.get())) + } + + fn of_class(c: ClassId) -> GroupId { + GroupId(u64::from(c.0)) + } + + /// Whether this group is keyed by a constructor script. Only ctor + /// instances are ever stamped, so a key range holding no ctor group + /// can never be hit at runtime. + fn is_ctor(self) -> bool { + self.0 & GroupId::CTOR_TAG != 0 + } + + /// The constructor script, for a ctor group. + fn ctor(self) -> Option { + self.is_ctor().then(|| ScriptId::new(self.0 as u32)) + } + + /// The class, for a group that is one class of its own. + fn class(self) -> Option { + (!self.is_ctor()).then_some(ClassId(self.0 as u32)) + } +} + +/// An ordered layout row under construction: the field names a constructor +/// or literal site installs, in first-write order. +/// +/// Bounded by `LAY_CAP` (a row longer than that is not a layout anybody +/// can guard on) and duplicate-free -- a field written twice keeps the +/// position of its first write, since that is where the slot was created. +#[derive(Default)] +struct LayoutRow { + names: Vec, + /// Names refused because the row was already full. + dropped: u64, +} + +impl LayoutRow { + fn push(&mut self, name: NameId) { + if self.names.contains(&name) { + return; + } + if self.names.len() >= LAY_CAP { + self.dropped += 1; + return; + } + self.names.push(name); + } + + fn into_names(self) -> Vec { + self.names + } +} + +/// Where one field name sits across a set of layout rows. +#[derive(Clone, Copy)] +struct NameFold { + /// The slot every row carrying the name put it at, or `None` once two + /// rows disagreed. Only a name at the same slot in every row can join + /// the group's universal prefix. + slot: Option, + /// How many of the folded rows carry the name at all. + rows: u32, +} + +/// The agreement between several layout rows. +/// +/// A group's members do not have to share a layout, but the leading fields +/// they *do* share -- same name, same slot, in every member -- are what a +/// range guard over the whole group can serve. Folding the rows together +/// and then reading off slot 0, 1, 2, ... until one disagrees is how that +/// common prefix is found. +#[derive(Default)] +struct SlotFold { + rows: u32, + names: HashMap, +} + +impl SlotFold { + /// Fold one layout row into the running agreement. + fn add_row(&mut self, row: &[NameId]) { + self.rows += 1; + for (i, name) in row.iter().enumerate() { + let slot = SlotIndex::new(u32::try_from(i).unwrap()); + match self.names.get_mut(name) { + None => { + self.names.insert( + *name, + NameFold { + slot: Some(slot), + rows: 1, + }, + ); + } + Some(e) => { + if e.slot != Some(slot) { + e.slot = None; + } + e.rows += 1; + } + } + } + } + + /// The names every folded row placed at the same slot, in slot order, + /// stopping at the first slot they do not all agree on. + fn universal_prefix(&self) -> Vec { + let mut by_slot: HashMap = HashMap::default(); + for (name, fold) in &self.names { + if let Some(s) = fold.slot { + if fold.rows == self.rows { + by_slot.insert(s, *name); + } + } + } + let mut prefix: Vec = Vec::new(); + let mut i = 0u32; + while (i as usize) < LAY_CAP { + match by_slot.remove(&SlotIndex::new(i)) { + Some(n) => { + prefix.push(n); + i += 1; + } + None => break, + } + } + prefix + } +} + +/// Expands a constructor's `this`-write events into the ordered layout row +/// its instances end up with. +/// +/// A constructor rarely installs every field itself: it hands `this` to +/// helpers (`Base.call(this, ...)`, `this.init(...)`) that install the +/// rest. The row is therefore the constructor's own writes with each +/// delegation target's row spliced in at the point of the call, which is +/// where those writes happen at runtime and so where the slots are +/// created. +/// +/// One expander serves a whole emission pass: it carries the memo and the +/// participant attribution across every constructor it is asked about, so +/// a helper shared by two constructors is expanded once and noticed as +/// shared. The two channels (with and without `this.m(...)` delegates) are +/// separate expanders, so neither channel's attribution disturbs the +/// other's. +struct CtorRowExpander<'a> { + /// Per-script `this` events, from the scan. + events: &'a HashMap>, + /// The single resolved `.call`/`.apply` target per site. + apply_targets: &'a HashMap, + /// The single scripted callee of a site, where it has exactly one: + /// what a `this.m(...)` init delegate resolves through. + single_call_target: &'a HashMap, + /// Whether to follow `this.m(...)` init delegates as well as + /// `.call`/`.apply` ones. The two-phase channel does; the row that + /// gets stamped at constructor exit does not, since those calls have + /// not happened yet. + follow_this_method_delegates: bool, + /// Rows already computed, one per script. + memo: HashMap>, + /// Delegation target -> the top-level constructor that splices it, + /// while exactly one does. A target spliced by one constructor can be + /// attributed to that constructor's layout; one spliced by two cannot. + participants: HashMap>, + /// What the row and depth caps refused while expanding (see + /// [`stats::CapDrops`]). + caps: CapDrops, +} + +impl<'a> CtorRowExpander<'a> { + fn new( + events: &'a HashMap>, + apply_targets: &'a HashMap, + single_call_target: &'a HashMap, + follow_this_method_delegates: bool, + ) -> CtorRowExpander<'a> { + CtorRowExpander { + events, + apply_targets, + single_call_target, + follow_this_method_delegates, + memo: HashMap::default(), + participants: HashMap::default(), + caps: CapDrops::default(), + } + } + + /// The layout row of constructor `top`. + fn expand(&mut self, top: ScriptId) -> Vec { + self.expand_under(top, top, 0) + } + + /// The memoization layer: a script's row does not depend on which + /// top-level constructor asked for it, so it is computed once. The + /// pre-insert of an empty row is also the cycle guard -- a delegation + /// cycle sees the empty row rather than recursing forever. + fn expand_under(&mut self, f: ScriptId, top: ScriptId, depth: u32) -> Vec { + if let Some(v) = self.memo.get(&f) { + return v.clone(); + } + if depth > MAX_DELEG_DEPTH { + self.caps.deleg_depth += 1; + return Vec::new(); + } + self.memo.insert(f, Vec::new()); + let row = self.collect(f, top, depth); + self.memo.insert(f, row.clone()); + row + } + + /// Walk `f`'s own events in program order, appending its writes and + /// splicing each delegation target's row where the call sits. + fn collect(&mut self, f: ScriptId, top: ScriptId, depth: u32) -> Vec { + let mut out = LayoutRow::default(); + let Some(events) = self.events.get(&f).cloned() else { + return out.into_names(); + }; + for ev in &events { + let target = match ev { + TEvent::Write(n) => { + out.push(*n); + None + } + TEvent::Deleg(pc) => self.apply_targets.get(&Site::new(f, *pc)).copied(), + TEvent::DelegM(pc) => self + .follow_this_method_delegates + .then(|| self.single_call_target.get(&Site::new(f, *pc)).copied()) + .flatten(), + }; + let Some(t) = target else { continue }; + if t == f { + continue; + } + self.note_participant(t, top); + for n in &self.expand_under(t, top, depth + 1) { + out.push(*n); + } + } + self.caps.layout_fields += out.dropped; + out.into_names() + } + + fn note_participant(&mut self, target: ScriptId, top: ScriptId) { + observe(&mut self.participants, target, top); + } +} + +/// The mask layout `key` claims for `name`, or empty when the layout does +/// not carry the name at all. +fn name_mask(rows: &HashMap, key: u32, name: NameId) -> Prims { + rows.get(&key) + .and_then(|r| { + let p = r.names.iter().position(|n| *n == name)?; + r.masks.get(p).copied() + }) + .unwrap_or(Prims::EMPTY) +} + +/// The per-slot masks of a universal prefix table over keys `lo..=hi`: the +/// all-members rule, so a slot claims only what every member of the range +/// claims, at that same slot. One member that puts a different name there, +/// or claims nothing, empties the slot for everybody -- the fact is read +/// through a range guard, so it has to hold for every key in range. +fn prefix_masks( + rows: &HashMap, + ptable: &[NameId], + lo: u32, + hi: u32, +) -> Vec { + ptable + .iter() + .enumerate() + .map(|(slot, name)| { + let mut bits = Prims::EMPTY; + for k in lo..=hi { + let m = name_mask(rows, k, *name); + let at_slot = rows.get(&k).and_then(|r| r.names.get(slot)) == Some(name); + if m == Prims::EMPTY || !at_slot { + bits = Prims::EMPTY; + break; + } + bits |= m; + } + bits + }) + .collect() +} + +// --- family 3: the predicted heap ---------------------------------------- + +/// Record a slot fact for `site`, if the site may take it. A read may +/// always take one; a write may only take a fact the whole key run agrees +/// on, since a store has to maintain the claim for whichever key its +/// receiver actually carries. +fn insert_prop_site(facts: &mut LikelyFacts, site: Site, f: SlotFact, is_read: bool) { + if !is_read && !f.uniform { + return; + } + facts.prop_sites.insert( + site, + ( + LayoutKey::new(f.lo), + LayoutKey::new(f.hi), + f.slot, + Claim::of_prims(f.prims), + ), + ); +} + +/// The entries of an agreement table that still agree, in key order. +/// +/// The tables this drains (`method_home`, an expander's `participants`) map +/// a script to the one constructor that claims it, and every consumer wants +/// the same thing: the settled pairs, deterministically ordered. +fn sole_participants(m: &HashMap>) -> Vec<(ScriptId, ScriptId)> { + let mut v: Vec<(ScriptId, ScriptId)> = m + .iter() + .filter_map(|(&k, a)| a.value().map(|c| (k, c))) + .collect(); + v.sort_unstable(); + v +} + +/// The predictor groups in key-assignment order. +/// +/// Region-contiguous: groups whose classes share a region (the class +/// union-find) occupy one contiguous super-range, so a region fact is a +/// range test over the same stamped key space. The sort key is (earliest +/// group of the region, group id), so singleton regions keep the plain +/// group-id order. +fn ordered_groups(sv: &Solver<'_>, rows: &RowSet) -> Vec { + let mut groups: Vec = super::sorted_keys(&rows.folds); + let mut region_rep: HashMap = HashMap::default(); + for &g in &groups { + if let Some(c) = sv.class_of_group(g) { + region_rep.entry(sv.engine.region_root(c)).or_insert(g); + } + } + // Regions with constructor rows key first: the construct-time early + // key is a 12-bit field (EARLY_KEY_MAX), and a program with enough + // object-literal classes can push every ctor key past it, leaving + // every construct site to seed a keyless word that no add can be + // checked against and no class can earn SLOTS through. Literal-only + // regions take the high keys; a literal's key is never seeded as an + // early key. + let has_ctor: HashMap = groups + .iter() + .filter_map(|&g| sv.class_of_group(g).map(|c| (sv.engine.region_root(c), g))) + .fold(HashMap::default(), |mut m, (r, g)| { + let e = m.entry(r).or_insert(false); + *e |= rows.ctor_rows.get(&g).is_some_and(|v| !v.is_empty()); + m + }); + groups.sort_by_key(|&g| match sv.class_of_group(g) { + Some(c) => { + let r = sv.engine.region_root(c); + ( + !has_ctor.get(&r).copied().unwrap_or(false), + region_rep[&r], + g, + ) + } + None => (rows.ctor_rows.get(&g).is_none_or(|v| v.is_empty()), g, g), + }); + groups +} + +/// One predicted layout row: the field names in slot order, with the claim +/// and the value range each position carries. +/// +/// Keying one map by a row makes "a key has all three, of the same length" +/// a type rather than a convention, and gives the mask/range pairing rule +/// (`pair_ranges`) exactly one place to be applied. +struct LayoutRowFacts { + names: Vec, + masks: Vec, + ranges: Vec>, +} + +/// The predicted-instance-layout half of the analysis: what each +/// constructor's objects look like, and the key space the guards range +/// over. +/// +/// This is one analysis with three products, and they have to be built +/// together because each one's shape constrains the next: +/// +/// - **Rows.** A constructor's layout row is the ordered list of fields its +/// instances get, expanded from its `this` writes with its delegates' +/// writes spliced in ([`CtorRowExpander`]). Object literals get a row +/// from their initializer order. +/// - **Keys.** Every row gets a dense [`LayoutKey`]. Keys are assigned so +/// that one predictor group is contiguous and one class region is +/// contiguous, which is what turns "is the receiver one of these +/// classes" into a range compare on the object's stamped class word. +/// - **Tables.** Members of a group rarely share a whole layout, but they +/// share a prefix; the fold of their rows ([`SlotFold`]) is the part a +/// range guard can serve, with the all-members mask rule. +/// +/// Nothing here reads a per-site fact -- it is the map the per-site +/// emission resolves against. +#[derive(Default)] +pub(super) struct LayoutPlan { + /// Layout key -> its row. + rows: HashMap, + /// Predictor group -> its contiguous key range. + group_range: HashMap, + /// Per-(script, formal): names written with the formal as receiver + /// and one write site each -- the arg-restamp derivation's input. + arg_fill: HashMap<(ScriptId, u32), Vec<(NameId, Site)>>, + /// (script, formal) pairs the body reassigns (SetArg): a return-time + /// stamp of the slot could stamp a different object. + arg_reassigned: HashSet<(ScriptId, u32)>, + /// Write sites per agreed receiver class and name, and every write + /// site whose receiver was a local or formal (the fill channel): a + /// fill row refuses when a suffix name is written on the class from + /// outside the channel, whose add order it cannot see. + class_writes: HashMap<(ClassId, NameId), Vec>, + fill_sites: HashSet, + /// Names written on a `this` with no agreed class at all. + unplaced_writes: HashSet, + /// Names written on a `this` agreed to each class. + this_writes: HashMap>, + /// Classes whose instances call a method whose `this` is pinned to + /// another class: two identities for one population (box2d's + /// b2Simplex, whose `ReadCache` is pinned to a rowless twin). A fill + /// row refuses a name its aliases write on `this`. + class_aliases: HashMap>, + /// The adds of every admitted fill run, and the run's reads of the + /// names it adds: they execute on an object still at the prefix key, + /// so a full-key fact there is a miss on every execution. + fill_add_sites: HashSet, + /// Group LO key -> the group's universal prefix table and its masks. + group_tables: HashMap, Vec)>, + /// Class region root -> the contiguous key range spanning its groups. + region_range: HashMap, + /// Class region root -> the region's universal prefix table and masks. + region_tables: HashMap, Vec)>, + /// Class-keyed groups whose key IS stamped (shared-ctor classes: the + /// init delegate restamps at its returns), unlike lit-only class + /// groups. + stamped_class_groups: HashSet, + /// Constructor -> (stamp key, full key). The two differ only for a + /// two-phase constructor, whose pair is key-adjacent: the prefix it + /// stamps at its own exit, and the full row its init delegate + /// completes. + ctor_key: HashMap, + /// Layout key -> the class whose view cell answers for it. Lit-row site + /// classes are deliberately absent -- an unmapped key makes the range + /// checks that consult this fail, which is the conservative direction. + key_class: HashMap, + /// Script -> the constructor whose layout its `this` is attributed to + /// (the constructor itself, a method homed to it, or a delegate only it + /// splices). + narrow: HashMap, + /// Next unassigned layout key. + next_key: u32, + /// What the layout caps refused, folded up from the expanders. + caps: CapDrops, +} + +/// One constructor's expanded rows: the prefix its own body installs, and +/// the full row when a `this.m(...)` init delegate extends it (two-phase +/// construction). +struct CtorRows { + ctor: ScriptId, + prefix: Vec, + full: Option>, +} + +/// The rows and per-group folds collected before any key is assigned. +#[derive(Default)] +struct RowSet { + folds: HashMap, + ctor_rows: HashMap>, + lit_rows: HashMap)>>, + /// Constructors whose full row extends their prefix row. + two_phase: HashSet, + /// Two-phase constructor -> the init delegates attributed to it. + tp_delegs: HashMap>, + /// Constructor whose full row a post-construction fill sequence + /// completes -> the fill sites (script, local, pc of the last add). + local_fills: HashMap>, + /// Delegation targets of the ctor-exit expansion, sorted: the scripts + /// whose `this.f = v` stores are instance inits rather than method-body + /// overwrites. + apply_delegates: Vec, +} + +impl LayoutPlan { + /// Run the whole layout analysis, filling the layout-shaped facts + /// (`ctor_stamps`, `ctor_nslots`, `deleg_restamps`, `this_layouts`, + /// `deleg_inits`, `construct_site_keys`) as it goes. + fn build(sv: &Solver<'_>, facts: &mut LikelyFacts, deleg: &Delegation) -> LayoutPlan { + let mut plan = LayoutPlan::default(); + for (ci, con) in sv.engine.cons.iter().enumerate() { + use super::engine::{CKey, Constraint}; + if let Constraint::Move { + dst: CKey::Arg(i), .. + } = con + { + let sid = sv.engine.con_script[ci]; + plan.arg_reassigned.insert((sid, i.get())); + } + } + for (&(sid, i), writes) in &sv.tables.arg_writes { + let list = plan.arg_fill.entry((sid, i.get())).or_default(); + for &(name, pc) in writes { + list.push((name, Site::new(sid, pc))); + } + } + for (ci, con) in sv.engine.cons.iter().enumerate() { + use super::engine::{CKey, Constraint}; + if let Constraint::Write { recv, name, pc, .. } = con { + let site = Site::new(sv.engine.con_script[ci], *pc); + let agreed = sv.site_recv_class.get(&site).and_then(Agreed::get); + if let Some(&c) = agreed { + plan.class_writes.entry((c, *name)).or_default().push(site); + } + if *recv == CKey::This { + match agreed { + Some(&c) => { + plan.this_writes.entry(c).or_default().insert(*name); + } + None => { + plan.unplaced_writes.insert(*name); + } + } + } + } + } + let mut callee_read: HashMap<(ScriptId, VarId), Site> = HashMap::default(); + for (ci, con) in sv.engine.cons.iter().enumerate() { + use super::engine::{CKey, Constraint}; + if let Constraint::Read { + dst: CKey::Var(v), + pc, + callee_pos: true, + .. + } = con + { + let sid = sv.engine.con_script[ci]; + callee_read.insert((sid, *v), Site::new(sid, *pc)); + } + } + for (ci, con) in sv.engine.cons.iter().enumerate() { + use super::engine::{CKey, Constraint}; + let Constraint::Call { + callee: CKey::Var(v), + this_: Some(_), + pc, + construct: false, + .. + } = con + else { + continue; + }; + let sid = sv.engine.con_script[ci]; + let Some(&rsite) = callee_read.get(&(sid, *v)) else { + continue; + }; + let Some(&a) = sv.site_recv_class.get(&rsite).and_then(Agreed::get) else { + continue; + }; + let Some(fns) = sv.site_calls.get(&Site::new(sid, *pc)) else { + continue; + }; + if fns.is_multi() { + continue; + } + for f in fns.ids() { + let Some(t) = f.as_script() else { continue }; + let Some(&b) = sv.this_pin.get(&t).and_then(Agreed::get) else { + continue; + }; + if b != a { + plan.class_aliases.entry(a).or_default().push(b); + plan.class_aliases.entry(b).or_default().push(a); + } + } + } + for (&(sid, _), writes) in &sv.tables.local_writes { + for &(_, pc) in writes { + plan.fill_sites.insert(Site::new(sid, pc)); + } + } + let rows = plan.collect_rows(sv, facts, deleg); + let groups = ordered_groups(sv, &rows); + plan.assign_keys(sv, facts, &rows, &groups); + plan.add_shared_ctor_classes(sv, facts, deleg); + plan.add_region_tables(sv); + plan.add_post_new_rows(sv, facts, deleg); + plan.emit_this_layouts(sv, facts, &rows); + plan + } + + /// Caller-side init-after-new: for each allocation site whose result + /// takes post-allocation `SetProp`s (the scan's `post_order` channel), + /// mint ONE extension row per site -- the site's base row (a shared + /// construct site's proto-keyed row, else the resolved ctor's full + /// row) extended by the site's own recorded order. The base is a + /// proper prefix of every extension, so the globally-recomputed + /// prefix relations turn the caller's adds into add-prediction pairs: + /// SLOTS and the epoch survive them, PER SITE -- no cross-site join + /// (different callers legitimately init different subsets in + /// different orders) and no new stamping (the base key stays the + /// stamped one). + fn add_post_new_rows(&mut self, sv: &Solver<'_>, facts: &mut LikelyFacts, deleg: &Delegation) { + let mut sites: Vec<(&Site, &Vec)> = sv.tables.post_order.iter().collect(); + sites.sort_unstable_by_key(|(s, _)| **s); + let mut seen: HashSet<(u32, Vec)> = HashSet::default(); + for (site, order) in sites { + if order.is_empty() { + continue; + } + let base_key = if let Some(k) = facts.construct_site_keys.get(site) { + k.get() + } else if let Some(&ctor) = deleg.single_call_target.get(site) { + match self.ctor_key.get(&ctor) { + Some(&(_, kf)) => kf, + None => continue, + } + } else { + continue; + }; + let Some(base) = self.rows.get(&base_key) else { + continue; + }; + let mut extended = base.names.clone(); + for &n in order { + if !extended.contains(&n) && extended.len() < LAY_CAP { + extended.push(n); + } + } + if extended.len() == self.rows[&base_key].names.len() { + continue; + } + if !seen.insert((base_key, extended.clone())) { + continue; + } + if self.next_key as usize >= LayoutKey::LIMIT as usize { + self.caps.layout_keys += 1; + break; + } + let class = self.key_class.get(&base_key).copied(); + self.add_row(sv, class, extended); + } + } + + /// Expand every constructed script and every object literal into its + /// layout row, and fold each group's rows together. + fn collect_rows( + &mut self, + sv: &Solver<'_>, + facts: &mut LikelyFacts, + deleg: &Delegation, + ) -> RowSet { + let mut rows = RowSet::default(); + // The ctor-exit row: `.call`/`.apply` delegates only, since a + // `this.m(...)` init call has not happened yet at the point the + // stamp goes in. + let mut prefix_rows = CtorRowExpander::new( + &sv.tables.this_events, + &deleg.apply_targets, + &deleg.single_call_target, + false, + ); + // The full row: `this.m(...)` init delegates spliced too. Its + // participant attribution is kept separate so a non-two-phase + // ctor's attribution is unaffected by this channel. + let mut full_rows = CtorRowExpander::new( + &sv.tables.this_events, + &deleg.apply_targets, + &deleg.single_call_target, + true, + ); + let mut constructed: Vec = sv.constructed.iter().copied().collect(); + constructed.sort_unstable(); + for &f in &constructed { + let prefix = prefix_rows.expand(f); + if prefix.is_empty() { + // Empty-prefix two-phase ctor -- an empty `function(){}` + // whose fields are all installed by a separate init + // delegate: there is no stampable ctor-exit prefix, but the + // Full expansion still names the allocation size. Record + // just the nslots, so construct sites allocate the full + // layout and the delegate's field adds ride the fixed-slot + // inline arms instead of the set-miss helper. No + // layout/stamp rows are minted. + let full = full_rows.expand(f); + if !full.is_empty() && full.len() <= LAY_CAP { + facts + .ctor_nslots + .insert(f, u32::try_from(full.len()).unwrap()); + } + continue; + } + let mut full = full_rows.expand(f); + let mut is_two_phase = full.len() > prefix.len() + && full.len() <= LAY_CAP + && full[..prefix.len()] == prefix[..]; + if !is_two_phase { + if let Some((row, fillers, adds)) = self.local_fill_row(sv, f, &prefix) { + full = row; + is_two_phase = true; + rows.local_fills.insert(f, fillers); + self.fill_add_sites.extend(adds); + } + } + let g = match sv.class_lookup_fn(f) { + Some(c) => sv.group_of_class(c), + None => GroupId::of_ctor(f), + }; + rows.folds.entry(g).or_default().add_row(&prefix); + let full = if is_two_phase { + rows.two_phase.insert(f); + rows.folds.entry(g).or_default().add_row(&full); + Some(full) + } else { + None + }; + rows.ctor_rows.entry(g).or_default().push(CtorRows { + ctor: f, + prefix, + full, + }); + } + // Literal sites: ordered init evidence on the site class; skip + // literals that became prototype objects (method tables). + let mut lit_sites: Vec<(Site, Vec)> = sv + .tables + .lit_order + .iter() + .map(|(&s, r)| (s, r.clone())) + .collect(); + lit_sites.sort_unstable_by_key(|(s, _)| *s); + for (site, order) in lit_sites { + if order.is_empty() { + continue; + } + if order.len() > LAY_CAP { + self.caps.layout_rows += 1; + continue; + } + if sv.heap.site_is_proto.contains(&site) { + continue; + } + if sv.heap.dyn_named_writes.contains(&site) { + continue; + } + let Some(c) = sv.heap.class_id(ClassKey::Site(site)) else { + continue; + }; + let g = sv.group_of_class(c); + rows.folds.entry(g).or_default().add_row(&order); + rows.lit_rows.entry(g).or_default().push((site, order)); + } + // Which script's `this` each layout is attributed to: the ctor + // itself, a method homed to it, or a delegate that exactly one ctor + // splices. + self.narrow_this_attribution(sv, &rows, &prefix_rows, &full_rows); + // Delegates of two-phase ctors, for the full-row mask lookup and + // the deleg_restamps emission. + for (d, c) in sole_participants(&full_rows.participants) { + if rows.two_phase.contains(&c) && d != c { + rows.tp_delegs.entry(c).or_default().push(d); + } + } + rows.apply_delegates = super::sorted_keys(&prefix_rows.participants); + self.caps.add(&prefix_rows.caps); + self.caps.add(&full_rows.caps); + rows + } + + /// The post-construction fill row of constructor `f`'s class: the + /// prefix extended by the names a straight-line add sequence on one + /// local receiver of the agreed class writes after construction + /// (box2d's `ccp = c.points[j]; ccp.normalImpulse = ...`). Writes + /// preceded by a read of the name off the same slot are overwrites and + /// do not count; every remaining run in the program must be a prefix + /// of the longest one (a partial fill on another path keeps the shape + /// order; a run in another order refuses the class), and the longest + /// becomes the row. The runs completing it restamp after their last + /// add. The stamp's runtime gates (ownership, slot span, the + /// add-prediction bits) refuse an instance filled any other way. + fn local_fill_row( + &self, + sv: &Solver<'_>, + f: ScriptId, + prefix: &[NameId], + ) -> Option<(Vec, Vec<(ScriptId, u32, Pc)>, Vec)> { + let class = sv.class_lookup_fn(f)?; + if prefix.is_empty() { + return None; + } + if sv.opts.diagnostics.propgap { + crate::diag_line!( + "night: fillrow ctor {} class {} prefix [{}]", + f.get(), + class.0, + prefix + .iter() + .map(|&n| String::from_utf16_lossy(sv.names.get(n))) + .collect::>() + .join(",") + ); + } + let mut keys: Vec<(ScriptId, u32)> = sv.tables.local_writes.keys().copied().collect(); + keys.sort_unstable(); + // One candidate per straight-line run of writes: a run ends at a + // control pc or a rebind of the local between two writes, so the + // arms of a branch never merge into one order. + let mut cands: Vec<(Vec, ScriptId, u32, Pc)> = Vec::new(); + let mut adds: Vec = Vec::new(); + for (s, l) in keys { + match sv.source.object(s.source()) { + crate::source::SourceObject::Script(sc) + if !sc.has_mapped_args && !sc.is_generator_or_async => {} + _ => continue, + } + let control = sv.tables.control_pcs.get(&s); + let sets = sv.tables.local_sets.get(&(s, l)); + let reads = sv.tables.local_reads.get(&(s, l)); + // A write preceded by a read of the same name off the same + // slot, with no rebind between, overwrites a field the object + // already has. + let overwrites = |n: NameId, pc: Pc| { + reads.is_some_and(|rs| { + rs.iter().any(|&(rn, rpc)| { + rn == n + && rpc < pc + && !sets.is_some_and(|v| v.iter().any(|&p| p > rpc && p < pc)) + }) + }) + }; + if sv.opts.diagnostics.propgap + && sv.tables.local_writes[&(s, l)].iter().any(|&(_, pc)| { + sv.site_recv_class + .get(&Site::new(s, pc)) + .and_then(Agreed::get) + == Some(&class) + }) + { + let ws: Vec = sv.tables.local_writes[&(s, l)] + .iter() + .map(|&(n, pc)| { + format!( + "{}@{}:{}", + String::from_utf16_lossy(sv.names.get(n)), + pc, + match sv.site_recv_class.get(&Site::new(s, pc)) { + Some(Agreed::One(c)) => c.0.to_string(), + Some(Agreed::Conflict) => "X".to_string(), + _ => "-".to_string(), + } + ) + }) + .collect(); + crate::diag_line!( + "night: fillrow writes class {} {}:l{} [{}]", + class.0, + s.get(), + l, + ws.join(" ") + ); + } + let breaks = |a: Pc, b: Pc| { + [control, sets] + .iter() + .any(|v| v.is_some_and(|v| v.iter().any(|&p| p > a && p < b))) + }; + let mut run: Vec = Vec::new(); + let mut run_first = Pc::new(0); + let mut run_last = Pc::new(0); + let mut prev: Option = None; + let mut run_adds: Vec = Vec::new(); + let close = |run: &mut Vec, + run_adds: &mut Vec, + first: Pc, + last: Pc, + cands: &mut Vec<(Vec, ScriptId, u32, Pc)>, + adds: &mut Vec| { + if run.is_empty() { + return; + } + cands.push((std::mem::take(run), s, l, last)); + adds.append(run_adds); + if let Some(rs) = reads { + adds.extend( + rs.iter() + .filter(|&&(_, rpc)| rpc > first && rpc < last) + .map(|&(_, rpc)| Site::new(s, rpc)), + ); + } + }; + for &(n, pc) in &sv.tables.local_writes[&(s, l)] { + if prev.is_some_and(|q| breaks(q, pc)) { + close( + &mut run, + &mut run_adds, + run_first, + run_last, + &mut cands, + &mut adds, + ); + } + prev = Some(pc); + if sv + .site_recv_class + .get(&Site::new(s, pc)) + .and_then(Agreed::get) + != Some(&class) + { + continue; + } + if prefix.contains(&n) || run.contains(&n) || overwrites(n, pc) { + continue; + } + if run.is_empty() { + run_first = pc; + } + run.push(n); + run_last = pc; + run_adds.push(Site::new(s, pc)); + } + close( + &mut run, + &mut run_adds, + run_first, + run_last, + &mut cands, + &mut adds, + ); + } + cands.retain(|c| prefix.len() + c.0.len() <= LAY_CAP); + let longest = cands.iter().map(|c| &c.0).max_by_key(|s| s.len())?.clone(); + let ordered = |a: &[NameId]| longest.starts_with(a); + let names = |ns: &[NameId]| { + ns.iter() + .map(|&n| String::from_utf16_lossy(sv.names.get(n))) + .collect::>() + .join(",") + }; + if sv.opts.diagnostics.propgap { + for c in &cands { + crate::diag_line!( + "night: fillrow class {} run {}:{} l{} [{}]{}", + class.0, + c.1.get(), + c.3, + c.2, + names(&c.0), + if ordered(&c.0) { "" } else { " REFUSES" } + ); + } + } + if !cands.iter().all(|c| ordered(&c.0)) { + return None; + } + let aliased = |n: NameId| { + self.class_aliases.get(&class).is_some_and(|bs| { + bs.iter() + .any(|b| self.this_writes.get(b).is_some_and(|ns| ns.contains(&n))) + }) + }; + let outside = longest.iter().find(|&&n| { + aliased(n) + || self + .class_writes + .get(&(class, n)) + .is_some_and(|sites| sites.iter().any(|s| !self.fill_sites.contains(s))) + }); + if let Some(&n) = outside { + if sv.opts.diagnostics.propgap { + crate::diag_line!( + "night: fillrow class {} REFUSES: {} is written outside the channel ({})", + class.0, + String::from_utf16_lossy(sv.names.get(n)), + if self.unplaced_writes.contains(&n) { + "unresolved this" + } else if aliased(n) { + "an alias class's this" + } else { + "agreed site" + } + ); + } + return None; + } + let fillers = cands + .iter() + .filter(|c| c.0 == longest) + .map(|c| (c.1, c.2, c.3)) + .collect(); + let mut row = prefix.to_vec(); + row.extend(longest); + Some((row, fillers, adds)) + } + + /// Fill `narrow`: the constructor each script's `this` belongs to. + fn narrow_this_attribution( + &mut self, + sv: &Solver<'_>, + rows: &RowSet, + prefix_rows: &CtorRowExpander<'_>, + full_rows: &CtorRowExpander<'_>, + ) { + let mut ctor_set: HashSet = HashSet::default(); + for group in rows.ctor_rows.values() { + for r in group { + ctor_set.insert(r.ctor); + self.narrow.insert(r.ctor, r.ctor); + } + } + let mut mh: Vec<(ScriptId, Option)> = sv + .heap + .method_home + .iter() + .map(|(&m, c)| (m, c.value())) + .collect(); + mh.sort_unstable(); + for (m, c) in mh { + if let Some(c) = c { + if ctor_set.contains(&c) { + self.narrow.entry(m).or_insert(c); + } + } + } + for (d, c) in sole_participants(&prefix_rows.participants) { + if ctor_set.contains(&c) { + self.narrow.entry(d).or_insert(c); + } + } + // Two-phase init delegates (`this.m(...)` splices) narrow to + // their sole splicing ctor like apply delegates -- but only for + // Two-phase ctors, so everything else keeps its attribution + // unchanged. + for (d, c) in sole_participants(&full_rows.participants) { + if rows.two_phase.contains(&c) { + self.narrow.entry(d).or_insert(c); + } + } + } + + /// Assign the dense layout keys, in `groups` order, and build the + /// per-group universal prefix tables. + fn assign_keys( + &mut self, + sv: &Solver<'_>, + facts: &mut LikelyFacts, + rows: &RowSet, + groups: &[GroupId], + ) { + for &root in groups { + let lo = self.next_key; + let rows_needed = rows.ctor_rows.get(&root).map_or(0, |r| { + r.iter().map(|e| 1 + usize::from(e.full.is_some())).sum() + }) + rows.lit_rows.get(&root).map_or(0, |r| r.len()); + if self.next_key as usize + rows_needed >= LayoutKey::LIMIT as usize { + self.caps.layout_keys += 1; + continue; + } + let mut ctor_rows: Vec<&CtorRows> = + rows.ctor_rows.get(&root).into_iter().flatten().collect(); + ctor_rows.sort_by(|x, y| x.prefix.cmp(&y.prefix).then(x.ctor.cmp(&y.ctor))); + let mut lit_rows: Vec<&(Site, Vec)> = + rows.lit_rows.get(&root).into_iter().flatten().collect(); + lit_rows.sort_by(|x, y| x.1.cmp(&y.1).then(x.0.cmp(&y.0))); + for cr in ctor_rows { + self.assign_ctor_key(sv, facts, rows, cr, lo); + } + for (site, row) in lit_rows { + self.assign_lit_key(sv, facts, *site, row.clone()); + } + let hi = self.next_key.wrapping_sub(1); + if self.next_key == lo { + continue; + } + self.group_range.insert(root, (lo, hi)); + let ptable = rows.folds[&root].universal_prefix(); + if !ptable.is_empty() && hi > lo { + let pmasks = prefix_masks(&self.rows, &ptable, lo, hi); + self.group_tables.insert(lo, (ptable, pmasks)); + } + } + // Layout key -> ClassId, for the two places that need to ask a + // *key* what its class's view cell says: the per-site + // type-dimension mask (which unions the member classes' claims + // across a key range) and the typed-tier layout claims. + for (&f, &(kp, kf)) in &self.ctor_key { + if let Some(c) = sv.class_lookup_fn(f) { + self.key_class.insert(kp, c); + self.key_class.insert(kf, c); + } + } + } + + /// Mint the key (or the adjacent key pair) of one constructor's row. + fn assign_ctor_key( + &mut self, + sv: &Solver<'_>, + facts: &mut LikelyFacts, + rows: &RowSet, + cr: &CtorRows, + group_lo: u32, + ) { + let f = cr.ctor; + // The stamped mask of a name under ctor `f` is read off the class + // view cell -- the estimate over the field's whole lifetime, not + // just the ctor's own writes. + let class = sv.class_lookup_fn(f); + let key = self.add_row(sv, class, cr.prefix.clone()); + facts.ctor_stamps.insert(f, LayoutKey::new(key)); + facts.ctor_nslots.insert( + f, + u32::try_from(cr.full.as_ref().map_or(cr.prefix.len(), Vec::len)).unwrap(), + ); + let Some(frow) = cr.full.as_ref() else { + self.ctor_key.insert(f, (key, key)); + return; + }; + let kf = self.add_row(sv, class, frow.clone()); + self.ctor_key.insert(f, (key, kf)); + for &(s, l, pc) in rows.local_fills.get(&f).into_iter().flatten() { + facts + .local_restamps + .insert(Site::new(s, pc), (l, LayoutKey::new(kf))); + } + // Pair prefix table (all-members masks over the pair), for method + // homes when the pair does not start its group (the group's own + // table then keys elsewhere). + if key != group_lo { + let pmasks: Vec = self.rows[&key] + .masks + .iter() + .zip(&self.rows[&kf].masks) + .map(|(&a, &b)| { + if a != Prims::EMPTY && b != Prims::EMPTY { + a | b + } else { + Prims::EMPTY + } + }) + .collect(); + self.group_tables + .insert(key, (self.rows[&key].names.clone(), pmasks)); + } + // Re-stamp tails only on delegates whose own direct writes + // contribute a suffix name (the row-completing scripts); + // transitively-reached delegates that write nothing on `this` would + // pay the tail on every hot return for no stamp. + let suffix = &frow[self.names(key).len()..]; + for &d in rows.tp_delegs.get(&f).into_iter().flatten() { + let completes = sv.tables.this_events.get(&d).is_some_and(|evs| { + evs.iter() + .any(|e| matches!(e, TEvent::Write(n) if suffix.contains(n))) + }); + if completes { + facts.deleg_restamps.entry(d).or_insert(LayoutKey::new(kf)); + } + } + // The formal-receiver siblings of the delegate rule: fill scripts + // completing the WHOLE suffix through one formal whose write sites + // agree on this ctor's class (the fresh-object-then-fill idiom -- + // `nbi()` handing the result to `multiplyTo(a, r)`). Each return + // re-stamps the formal's object under the same validated-shape + // gates, so an unfilled early-return receiver just refuses. The + // whole-suffix requirement keeps the per-return tail off scripts + // whose stamp could never pass the span gate. + if !suffix.is_empty() { + if let Some(class) = class { + let mut cands: Vec<(ScriptId, u32)> = Vec::new(); + for (&(s, i), writes) in &self.arg_fill { + if s == f + || self.arg_reassigned.contains(&(s, i)) + || facts.deleg_restamps.contains_key(&s) + { + continue; + } + match sv.source.object(s.source()) { + crate::source::SourceObject::Script(sc) + if !sc.has_mapped_args && !sc.is_generator_or_async => {} + _ => continue, + } + let covered = suffix.iter().all(|n| { + writes.iter().any(|(wn, site)| { + wn == n + && sv + .site_recv_class + .get(site) + .and_then(super::types::Agreed::get) + == Some(&class) + }) + }); + if covered { + cands.push((s, i)); + } + } + cands.sort_unstable(); + for (s, i) in cands { + facts + .arg_restamps + .entry(s) + .or_insert((i, LayoutKey::new(kf))); + } + } + } + } + + /// Mint the key of one object-literal site's row. The joined read + /// evidence has no per-class cell here, so the masks come from the site + /// class's view cell directly. + fn assign_lit_key( + &mut self, + sv: &Solver<'_>, + facts: &mut LikelyFacts, + site: Site, + row: Vec, + ) { + let class = sv.heap.class_id(ClassKey::Site(site)); + let key = self.add_row(sv, class, row); + facts.lit_stamps.insert(site, LayoutKey::new(key)); + } + + /// Region ranges and tables (the fenced hierarchy's middle rung): a + /// region with two or more keyed groups spans their key ranges -- + /// contiguous by the ordering `assign_keys` used -- and its table is + /// the fold over every member row, masks by the all-members rule. + fn add_region_tables(&mut self, sv: &Solver<'_>) { + // Every keyed group, not the ordered row list: shared-generated-ctor + // classes (the prototype.js `Class.create()` idiom) key their own + // single-key groups in `add_shared_ctor_classes`, which runs first + // -- built from the row list alone, a region whose members are all + // such classes had no range at all and every region-typed site + // resolved against nothing. + let mut region_groups: HashMap> = HashMap::default(); + for &g in self.group_range.keys() { + if let Some(c) = sv.class_of_group(g) { + region_groups + .entry(sv.engine.region_root(c)) + .or_default() + .push(g); + } + } + for (&r, groups) in ®ion_groups { + if groups.len() < 2 { + continue; + } + // A region fact's range guard tests the stamped header key, so + // some key in range must actually get stamped: ctor instances + // stamp at the ctor's exit, shared-ctor classes restamp at + // their init delegate's returns. A range holding only lit keys + // can never hit, so every read through it pays the miss for + // nothing. + if !groups + .iter() + .any(|g| g.is_ctor() || self.stamped_class_groups.contains(g)) + { + continue; + } + let lo = groups.iter().map(|g| self.group_range[g].0).min().unwrap(); + let hi = groups.iter().map(|g| self.group_range[g].1).max().unwrap(); + self.region_range.insert(r, (lo, hi)); + // The spanning range may interleave keys of groups outside the + // region, and the emitted guard admits any key in it -- so the + // prefix folds every key in the span, member or not. + let mut fold = SlotFold::default(); + for k in lo..=hi { + let row = self.names(k).to_vec(); + fold.add_row(&row); + } + let ptable = fold.universal_prefix(); + if ptable.is_empty() { + continue; + } + let pmasks = prefix_masks(&self.rows, &ptable, lo, hi); + self.region_tables.insert(r, (ptable, pmasks)); + } + } + + /// Shared-generated-ctor classes (the prototype.js `Class.create()` + /// idiom): the pre-solve concrete resolution (`resolve_shared_ctor_sites`) + /// mapped each construct site to (ctor script, prototype object, + /// init-delegate script) and the model already ran with the + /// per-prototype classes. Here each distinct prototype mints a layout + /// key whose row is the init delegate's expansion; the delegate rides + /// the existing deleg_restamps/deleg_inits/this_layouts rails (static + /// add checks + full-key restamp at its returns), and + /// `construct_site_keys` seeds the keyed alloc word per site. + fn add_shared_ctor_classes( + &mut self, + sv: &Solver<'_>, + facts: &mut LikelyFacts, + deleg: &Delegation, + ) { + use crate::source::{ObjectData, SourceObject, SourceObjectId}; + let mut sites: Vec<(Site, &SharedCtorSite)> = + sv.shared_ctor_sites.iter().map(|(&s, v)| (s, v)).collect(); + sites.sort_unstable_by_key(|(s, _)| *s); + // Per distinct prototype object: the minted layout key. + let mut proto_key: HashMap = HashMap::default(); + // Method home candidates: member script -> the keys claiming it. + let mut method_homes: HashMap> = HashMap::default(); + for (site, shared) in sites { + let (f, proto, init_sid) = (shared.ctor, shared.proto, shared.init); + if self.next_key as usize >= LayoutKey::LIMIT as usize { + self.caps.layout_keys += 1; + break; + } + // A ctor the script-keyed machinery already rows (the + // single-class apply-delegate idiom) keeps that path whole: + // minting a second proto-keyed key for the same class makes the + // alloc-seeded and delegate-restamped ids fight, and the two + // then disagree about which slots the object has. + if facts.ctor_stamps.contains_key(&f) { + continue; + } + let key = match proto_key.get(&proto) { + Some(&k) => k, + None => { + if facts.ctor_stamps.contains_key(&init_sid) + || facts.deleg_restamps.contains_key(&init_sid) + { + continue; + } + let mut expander = CtorRowExpander::new( + &sv.tables.this_events, + &deleg.apply_targets, + &deleg.single_call_target, + true, + ); + let row = expander.expand(init_sid); + self.caps.add(&expander.caps); + if row.is_empty() { + continue; + } + if row.len() > LAY_CAP { + self.caps.layout_rows += 1; + continue; + } + // Masks from the per-prototype class view (the model + // ran with these classes, so the views are + // class-precise). + let pcl = sv.heap.class_id(ClassKey::Proto(proto)); + let key = self.add_row(sv, pcl, row); + // The class keys its own group (ctor: None, the + // lit-class rule), so registering the exact key range + // here is what lets the per-site emission resolve + // receivers of this class. + if let Some(c) = pcl { + self.group_range.insert(GroupId::of_class(c), (key, key)); + self.key_class.insert(key, c); + self.stamped_class_groups.insert(GroupId::of_class(c)); + } + // Scripted members of the prototype (the class's + // methods) are this-home candidates. + if let SourceObject::Object(ObjectData { properties, .. }) = + sv.source.object(proto) + { + for (_, v) in properties { + if let Some(m) = sv.source.fn_script(*v) { + method_homes.entry(m).or_default().push(key); + } + } + } + facts.deleg_restamps.insert(init_sid, LayoutKey::new(key)); + facts + .this_layouts + .entry(init_sid) + .or_insert((LayoutKey::new(key), LayoutKey::new(key))); + proto_key.insert(proto, key); + key + } + }; + facts.construct_site_keys.insert(site, LayoutKey::new(key)); + } + // Method this-homes: only members claimed by exactly one class and + // not already homed. + for (m, keys) in { + let mut v: Vec<_> = method_homes.into_iter().collect(); + v.sort_unstable(); + v + } { + if let [k] = keys.as_slice() { + if !facts.ctor_stamps.contains_key(&m) { + facts + .this_layouts + .entry(m) + .or_insert((LayoutKey::new(*k), LayoutKey::new(*k))); + } + } + } + } + + /// Per-method this-layouts: for each script that is not itself a + /// constructor, the layout key (or key range) its `this` is predicted + /// to carry. The lowering consumes this at method entry -- + /// `wasm::bbv::facts` primes the shape/generation cell from it, and + /// `wasm::bbv::property` serves fixed-slot reads off `this` against it + /// without a per-site class check. Two keys because a two-phase + /// constructor stamps a prefix key at its own exit and the full key at + /// its init delegate's, so a method that may see either guards the + /// range. + fn emit_this_layouts(&self, sv: &Solver<'_>, facts: &mut LikelyFacts, rows: &RowSet) { + for m in super::sorted_keys(&sv.engine.script_cons) { + if facts.ctor_stamps.contains_key(&m) { + continue; + } + if let Some(&c) = self.narrow.get(&m) { + if let Some(&(kp, kf)) = self.ctor_key.get(&c) { + facts + .this_layouts + .insert(m, (LayoutKey::new(kp), LayoutKey::new(kf))); + continue; + } + } + let Some(c) = sv.script_this_class(m) else { + continue; + }; + let Some((lo, hi)) = self.range_of(sv.group_of_class(c)) else { + continue; + }; + if lo == hi { + facts + .this_layouts + .insert(m, (LayoutKey::new(lo), LayoutKey::new(lo))); + } else if self.group_tables.contains_key(&lo) { + facts + .this_layouts + .insert(m, (LayoutKey::new(lo), LayoutKey::new(hi))); + } + } + // Delegates whose `this.f = v` stores are instance inits rather + // than method-body overwrites: the layout-set slow tail carries the + // add-transition arm there, and only there (it is pure bloat on a + // method-overwrite tail). Both delegation channels qualify, and a + // script that is itself a stamped ctor does not. + for &p in &rows.apply_delegates { + if !facts.ctor_stamps.contains_key(&p) { + facts.deleg_inits.insert(p); + } + } + for p in super::sorted_keys(&facts.deleg_restamps) { + if !facts.ctor_stamps.contains_key(&p) { + facts.deleg_inits.insert(p); + } + } + } + + pub(super) fn range_of(&self, g: GroupId) -> Option<(u32, u32)> { + self.group_range.get(&g).copied() + } + + fn name_mask(&self, key: u32, name: NameId) -> Prims { + name_mask(&self.rows, key, name) + } + + /// The field names of a layout row, or nothing if the key has none. + fn names(&self, key: u32) -> &[NameId] { + self.rows.get(&key).map_or(&[], |r| &r.names) + } + + /// Mint a key for `names`, reading each position's claim and range off + /// `class`'s view cells. The one place a layout row is created, so the + /// one place the mask/range pairing has to hold. + fn add_row(&mut self, sv: &Solver<'_>, class: Option, names: Vec) -> u32 { + let key = self.next_key; + self.next_key += 1; + let masks: Vec = names + .iter() + .map(|n| { + class + .and_then(|c| sv.class_view_prims(c, *n)) + .unwrap_or(Prims::EMPTY) + }) + .collect(); + let ranges = pair_ranges( + &masks, + names + .iter() + .map(|n| class.and_then(|c| sv.class_view_range(c, *n))) + .collect(), + ); + self.rows.insert( + key, + LayoutRowFacts { + names, + masks, + ranges, + }, + ); + key + } + + /// `name`'s position in a universal prefix table over `lo..=hi`, with + /// the claim that position makes. `masks` is the table's own mask row + /// where it has one (a group or region table) and `None` for an exact + /// single-key row, where the layout's own mask answers. + fn table_slot_fact( + &self, + lo: u32, + hi: u32, + table: &[NameId], + masks: Option<&[Prims]>, + name: NameId, + ) -> Option { + let pos = table.iter().position(|n| *n == name)?; + let slot = SlotIndex::new(u32::try_from(pos).unwrap()); + let prims = match masks { + Some(ms) => ms.get(pos).copied().unwrap_or(Prims::EMPTY), + None => self.name_mask(lo, name), + }; + // A single-key run claims uniformly by definition; over a range, + // either the table claims for everybody or nobody may. + let uniform = lo == hi + || !prims.is_empty() + || (lo..=hi).all(|k| self.name_mask(k, name) == Prims::EMPTY); + Some(SlotFact { + lo, + hi, + slot, + prims, + uniform, + }) + } + + /// The longest run of adjacent keys in `glo..=ghi` that all place + /// `name` at the same slot, with the claim over that run. + /// + /// A group's universal prefix table only covers names every member + /// agrees on; this is the fallback for a name only part of the group + /// carries, and it narrows the fact's range guard to exactly the keys + /// that agree. `uniform` says whether every key in the run claims (so a + /// store site may maintain the claim) or none does. + fn subrange_in(&self, glo: u32, ghi: u32, name: NameId) -> Option { + if glo == ghi { + return None; + } + let mut best: Option<(u32, u32, SlotIndex)> = None; + let mut run: Option<(u32, SlotIndex)> = None; + for k in glo..=ghi { + let pos = self + .names(k) + .iter() + .position(|n| *n == name) + .map(|p| SlotIndex::new(u32::try_from(p).unwrap())); + match (pos, run) { + (Some(s), Some((rl, rs))) if s == rs => { + let len = k - rl + 1; + if best.is_none_or(|(bl, bh, _)| len > bh - bl + 1) { + best = Some((rl, k, s)); + } + } + (Some(s), _) => { + run = Some((k, s)); + if best.is_none() { + best = Some((k, k, s)); + } + } + (None, _) => { + run = None; + } + } + } + let (lo, hi, slot) = best?; + // A run that leaves out group members which ALSO carry the name + // is a coin flip: a receiver of any excluded member misses the + // guard on every execution (a run that omits a group member which + // carries the name at a different slot leaves that member + // permanently unstamped for it). No fact beats a wrong one: the + // site takes the IC, which serves every member. + let conflict = (glo..=ghi) + .filter(|k| *k < lo || *k > hi) + .any(|k| self.names(k).contains(&name)); + if conflict { + return None; + } + let mut bits = Prims::EMPTY; + let mut claimed = 0u32; + for k in lo..=hi { + let m = self + .rows + .get(&k) + .and_then(|r| r.masks.get(slot.get() as usize)) + .copied() + .unwrap_or(Prims::EMPTY); + if m != Prims::EMPTY { + claimed += 1; + } + bits |= m; + } + let n = hi - lo + 1; + let all_claim = claimed == n; + Some(SlotFact { + lo, + hi, + slot, + prims: if all_claim { bits } else { Prims::EMPTY }, + uniform: all_claim || claimed == 0, + }) + } +} + +/// A range row is meaningful only where the mask row claims: pair them at +/// every insertion so the two can never drift apart. +/// +/// Narrowed further to INT32-only masks. The store-side check that +/// maintains a range is then two compares on the int32 payload, licensed by +/// the tag check the mask arm already emits; an int|double field would need +/// the same bounds tested in f64 as well, which buys nothing on the +/// populations this targets (integer digit arrays and plain int fields). +fn pair_ranges(masks: &[Prims], ranges: Vec>) -> Vec> { + masks + .iter() + .zip(ranges) + .map(|(&m, r)| { + if m == Prims::from_bits(PRIM_INT32.bits()) { + r + } else { + None + } + }) + .collect() +} + +/// The slot half of a property fact: which keys agree, where the field +/// sits in them, and what the position claims. +#[derive(Clone, Copy)] +struct SlotFact { + lo: u32, + hi: u32, + slot: SlotIndex, + prims: Prims, + /// Whether every key in the run claims, or none does. A read may take + /// the fact either way; a write may only maintain a claim the whole + /// run makes, since it cannot know which key its receiver carries. + uniform: bool, +} + +/// A write site's name-keyed typed mask, held back until every read site +/// has been seen: the mask upgrades the store fence, which is pure cost +/// unless some typed read actually consumes the position it covers. +struct PendingTypedWrite { + site: Site, + row: (LayoutKey, LayoutKey, Claim), + /// The layout slot the site's prop fact settled on, when its key range + /// matches the typed row's -- only then can a read consume, or a write + /// maintain, the position. + slot: Option, +} + +/// The delegation edges the layout analysis walks through. Analysis- +/// internal: codegen keys on `apply_sites` rather than a resolved apply +/// target, since the forward helper reads the real one off the stack. +struct Delegation { + /// The single resolved `.call`/`.apply` target per site. The form it + /// spells is not carried: codegen reads that off `apply_sites`, and + /// the layout walk only needs to know where the delegation goes. + apply_targets: HashMap, + /// The single scripted callee per ordinary call site: what a + /// `this.m(...)` init delegate resolves through. + single_call_target: HashMap, +} + +/// What `emit_site_facts` learned that the phases after it need. +#[derive(Default)] +struct SiteFactTotals { + /// (key, slot) positions some typed read site actually consumes. + typed_read_positions: HashSet<(LayoutKey, SlotIndex)>, + /// Element read sites an array claim could fold at. + array_fold_reads: u32, + /// Element write sites whose receiver did not resolve to one array + /// population, and which therefore owe the maintenance duty + /// unconditionally. + array_unresolved_writes: u32, +} + +impl Solver<'_> { + /// Non-minting class lookup for a ctor script. + fn class_lookup_fn(&self, f: ScriptId) -> Option { + let key = match self.heap.script_proto.get(&f) { + Some(&p) => ClassKey::Proto(p), + None => ClassKey::Script(f), + }; + self.heap.class_id(key) + } + + /// The predictor group a class belongs to. + pub(super) fn group_of_class(&self, c: ClassId) -> GroupId { + match self.heap[c].ctor { + Some(f) => GroupId::of_ctor(f), + None => GroupId::of_class(c), + } + } + + /// The class a group speaks for, if it has one. + fn class_of_group(&self, g: GroupId) -> Option { + match g.ctor() { + Some(f) => self.class_lookup_fn(f), + None => g.class(), + } + } + + pub(super) fn emit(&mut self) -> LikelyFacts { + let mut facts = LikelyFacts::default(); + let mut caps = CapDrops::default(); + self.emit_value_claims(&mut facts); + let deleg = self.emit_call_sites(&mut facts, &mut caps); + // The analysis half of the speculation trace (see `viz`). + if let Some(mut out) = super::viz::stream(self.opts) { + super::viz::write_arg_types(self, &mut out); + super::viz::write_gname_cells(self, &self.names, &mut out); + super::viz::write_field_cells(self, &self.names, &mut out); + super::viz::write_regions(self, &mut out); + super::viz::write_arith_dsts(self, &mut out); + } + let plan = LayoutPlan::build(self, &mut facts, &deleg); + caps.add(&plan.caps); + let totals = self.emit_site_facts(&mut facts, &plan); + self.emit_arg_cls(&mut facts, &plan); + self.emit_class_rows(&mut facts, &plan, &totals.typed_read_positions); + self.emit_array_claims(&mut facts, &totals); + super::effects::emit_effect_summaries(self, &mut facts, &plan); + facts.n_classes = self.heap.classes.len(); + facts.n_cons = self.engine.cons.len(); + // Drops the fixpoint recorded but never reported, plus everything + // the emission phase refused. + caps.add(&self.tables.caps); + caps.fn_set = u64::try_from(self.engine.sink.dropped_fns.len()).unwrap(); + caps.snap_absorb = u64::try_from(self.engine.sink.snap_absorbs.len()).unwrap(); + self.stats.caps.add(&caps); + facts + } + + // --- family 1: value claims ------------------------------------------ + + /// The dataflow facts: what a value at a program point is likely to be. + /// Three producers, all the same shape -- take a cell, join it over the + /// contexts its script was live at, and run the result through a claim + /// tier. + /// Per-formal VALUE class, the advisory sibling of the `arg_types` + /// claims: the arg cell's obj half joined over live ctxs, mapped + /// through the plan's key ranges exactly like the per-site value + /// classes. Post-plan (the mapping needs the key space); regions + /// accepted. + fn emit_arg_cls(&self, facts: &mut LikelyFacts, plan: &LayoutPlan) { + use super::engine::CellKey; + use super::heap::RegionLabels; + for (&sid, ctxs) in &self.engine.live_ctxs { + for i in 1..=MAX_TRACKED_FORMALS { + let mk = |ctx| CellKey::Arg { + script: sid, + arg: FormalIndex::new(i - 1), + ctx, + }; + let Some(j) = self.engine.join_over_ctxs(ctxs, mk) else { + continue; + }; + let Some(Some(vc)) = self.recv_class(j.obj, j.unknown, RegionLabels::Accept) else { + continue; + }; + let range = if self.engine.region_root(vc) == vc + && self + .engine + .region_members + .get(&vc) + .is_some_and(|ms| ms.len() > 1) + { + plan.region_range.get(&vc).copied() + } else { + plan.range_of(self.group_of_class(vc)) + }; + if let Some((lo, hi)) = range { + facts.arg_cls.insert( + (sid, ArgIndex::new(i)), + (LayoutKey::new(lo), LayoutKey::new(hi)), + ); + } + } + } + } + + fn emit_value_claims(&self, facts: &mut LikelyFacts) { + use super::engine::{CKey, CellKey, Constraint}; + // Per-script this/arg claims (the guard-at-defs family), projected + // to either a purely-numeric mask or the object-only claim. Mixed + // prim/object evidence and unresolved evidence emit nothing. + for (&sid, ctxs) in &self.engine.live_ctxs { + for i in 0..=MAX_TRACKED_FORMALS { + let mk = |ctx| { + if i == 0 { + CellKey::This { script: sid, ctx } + } else { + CellKey::Arg { + script: sid, + arg: FormalIndex::new(i - 1), + ctx, + } + } + }; + let Some(j) = self.engine.join_over_ctxs(ctxs, mk) else { + continue; + }; + let Some(claim) = j.value_claim_full() else { + continue; + }; + facts.arg_types.insert((sid, ArgIndex::new(i)), claim); + } + } + // Receiver demand (the guard-at-defs demand filter): Object claims + // are emitted only for defs whose result is consumed as an element + // receiver -- there the guard's tag test migrates (the consumer's + // receiver test elides, dead non-object arms die). An unconsumed + // object proof is a per-read cost with no payer, and claiming them + // blanket-wide is a substantial loss. Property receivers guard on + // class-idx words, which a bare object proof does not elide, so an + // object claim there is pure cost. Numeric claims stay demand-free. + let recv_demand: HashSet<(ScriptId, CKey)> = { + let mut d = HashSet::default(); + for ci in 0..self.engine.cons.len() { + let sid = self.engine.con_script[ci]; + match &self.engine.cons[ci] { + Constraint::Read { recv, name, .. } | Constraint::Write { recv, name, .. } + if *name == self.names_of.elems => + { + d.insert((sid, *recv)); + } + _ => {} + } + } + d + }; + // Call-result per-site claims (the call def family): each call + // constraint's ret var joined over live ctxs, through the full + // claim tier -- numeric demand-free, object only under receiver + // demand. + let elem_pcs: HashSet<(ScriptId, Pc)> = (0..self.engine.cons.len()) + .filter_map(|ci| match &self.engine.cons[ci] { + Constraint::ElemBuiltin { pc, .. } => Some((self.engine.con_script[ci], *pc)), + _ => None, + }) + .collect(); + for ci in 0..self.engine.cons.len() { + let Constraint::Call { ret, pc, .. } = &self.engine.cons[ci] else { + continue; + }; + let CKey::Var(def) = *ret else { continue }; + let (ret, pc) = (*ret, *pc); + let sid = self.engine.con_script[ci]; + // An array builtin's result is the element-node model's, and + // the builtin arm has no use for a claim on it. + if elem_pcs.contains(&(sid, pc)) { + continue; + } + let Some(ctxs) = self.engine.live_ctxs.get(&sid) else { + continue; + }; + let Some(j) = self.engine.join_over_ctxs(ctxs, |ctx| CellKey::Var { + script: sid, + var: def, + ctx, + }) else { + continue; + }; + let Some(m) = j.site_claim() else { continue }; + if m.is_object() && !recv_demand.contains(&(sid, ret)) { + continue; + } + facts.call_types.insert(Site::new(sid, pc), m); + } + // Fractional-reachable arith sites: the result var of each arith + // constraint, joined over live ctxs -- double evidence at range Top + // means a real double population flows through the op, and its + // both-number arm may keep the Opt track (the numeric-category + // policy). See LikelyFacts::fractional_arith_sites. + for ci in 0..self.engine.cons.len() { + let Constraint::Arith { dst, pc, .. } = &self.engine.cons[ci] else { + continue; + }; + let CKey::Var(def) = *dst else { continue }; + let pc = *pc; + let sid = self.engine.con_script[ci]; + let Some(ctxs) = self.engine.live_ctxs.get(&sid) else { + continue; + }; + let Some(j) = self.engine.join_over_ctxs(ctxs, |ctx| CellKey::Var { + script: sid, + var: def, + ctx, + }) else { + continue; + }; + if j.fractional_reachable() { + facts.fractional_arith_sites.insert(Site::new(sid, pc)); + } + if j.string_reachable() { + facts.string_arith_sites.insert(Site::new(sid, pc)); + } + } + // Aliased-var per-site claims (the closure-scope analog of the + // elem value claims): each statically resolved GetAliasedVar site + // projects its (scope, slot) cell through the purely-numeric gate. + for read in &self.tables.aliased_reads { + let key = CellKey::Aliased { + scope: read.scope, + slot: read.slot, + }; + let Some(cid) = self.engine.lookup(key) else { + continue; + }; + let Some(m) = self.engine.ts(cid).value_claim_full() else { + continue; + }; + if m.is_object() + && !recv_demand.contains(&( + read.site.script, + CKey::Aliased { + scope: read.scope, + slot: read.slot, + }, + )) + { + continue; + } + facts.aliased_sites.insert(read.site, m); + } + // Gname value claims (the guard-at-defs family applied to the + // global store): each context-free GName cell through the full + // claim tier. Numeric claims demand-free; object claims only where + // some script consumes the name as an element receiver (the + // arg_types discipline -- a blanket object proof is a per-read + // cost with no payer). + let gname_recv_demand: HashSet = recv_demand + .iter() + .filter_map(|&(_, k)| match k { + CKey::GName(n) => Some(n), + _ => None, + }) + .collect(); + for (name, cid) in self.engine.gname_cells() { + let ts = self.engine.ts(cid); + let Some(m) = ts.value_claim_full().or_else(|| ts.object_claim_nullish()) else { + continue; + }; + if m.is_object() && !gname_recv_demand.contains(&name) { + continue; + } + // An element-receiver global whose every value is one + // typed-array kind claims the kind too: the read's ladder tests + // the clasp once and the element ops on the value skip theirs. + let m = match (m.is_object(), self.obj_ta_kind(ts.obj)) { + (true, Some(k)) => Claim::object_of_ta(k), + _ => m, + }; + facts.gname_types.insert(name, m); + } + } + + // --- family 2: call-site resolution ---------------------------------- + + /// How each call site resolved, plus the delegation edges the layout + /// analysis walks through. + fn emit_call_sites(&self, facts: &mut LikelyFacts, caps: &mut CapDrops) -> Delegation { + // Scripted targets: 1..=MAX_SITE_TARGETS, emitted as a guard chain. + for (&site, fns) in &self.site_likely_calls { + if fns.is_multi() || fns.is_empty() { + continue; + } + if fns.ids().len() > MAX_SITE_TARGETS { + caps.call_targets += 1; + continue; + } + facts.call_sites.insert( + site, + CallResolution::Scripted(fns.ids().iter().filter_map(|f| f.as_script()).collect()), + ); + } + // accessor_sites: property sites whose agreeing receiver class + // carries a modeled defineProperty accessor for the name. The + // target also resolves the site as a scripted call so the + // accessor-call arm inherits the likely-callee machinery (funcidx + // patch, typed entries). + if !self.accessors.is_empty() { + for &(_, n) in self.accessors.keys() { + facts.accessor_names.insert(n); + } + for ci in 0..self.engine.cons.len() { + let (name, pc, is_write) = match &self.engine.cons[ci] { + super::engine::Constraint::Read { name, pc, .. } => (*name, *pc, false), + super::engine::Constraint::Write { name, pc, .. } => (*name, *pc, true), + _ => continue, + }; + let sid = self.engine.con_script[ci]; + let site = Site::new(sid, pc); + let Some(&c) = self.site_recv_class.get(&site).and_then(Agreed::get) else { + continue; + }; + let Some(&(g, s)) = self.accessors.get(&(c, name)) else { + continue; + }; + let target = if is_write { s } else { g }; + let Some(target) = target else { continue }; + facts + .accessor_sites + .insert(site, (target, u8::from(is_write))); + facts + .call_sites + .entry(site) + .or_insert_with(|| CallResolution::Scripted(vec![target])); + } + } + // Native resolution: sites settled on one bare-name modeled native + // the translator has an arm for. The runtime callee native-pointer + // guard makes a wrong resolution a missed fast path, never a + // miscompile. Exclusive with the scripted arm by construction: a + // site resolves to one native only if every evaluation saw exactly + // that native, which leaves no scripted id for `site_calls`. + for (&site, id) in &self.site_native { + let Some(&id) = id.get() else { continue }; + let Some(info) = self.natives.get(id) else { + continue; + }; + if info.kind != super::builtins::NativeKind::Bare { + continue; + } + if super::builtins::has_translator_arm(self.names.get(info.name)) { + debug_assert!( + !facts.call_sites.contains_key(&site), + "call site resolved both native and scripted" + ); + facts.call_sites.insert(site, CallResolution::Native); + } + } + for (&site, &form) in &self.tables.apply_sites { + facts.apply_sites.insert(site, form); + } + let is_hasown = |chars: &[u16]| super::builtins::name_eq(chars, "hasOwnProperty"); + for (&site, id) in &self.site_apply_native { + let Some(&id) = id.get() else { continue }; + let Some(info) = self.natives.get(id) else { + continue; + }; + if info.kind == super::builtins::NativeKind::Bare + && is_hasown(self.names.get(info.name)) + { + facts + .apply_natives + .insert(site, crate::facts::ApplyNative::HasOwnProperty); + } + } + // A self-hosted builtin transcribed with its script + // (`Object.prototype.hasOwnProperty`) is a mono SCRIPTED apply + // target; a function object's own name says which. The arm's + // identity guard makes a same-named user function a miss. + let mut hasown_scripts: HashSet = HashSet::default(); + for (_, obj) in self.source.objects() { + if let crate::source::SourceObject::Object(crate::source::ObjectData { + kind: crate::source::ObjectKind::Function, + script: Some(s), + name: Some(n), + .. + }) = obj + { + if let crate::source::SourceObject::String(st) = self.source.object(*n) { + if is_hasown(st.chars()) { + hasown_scripts.insert(ScriptId::new(s.id())); + } + } + } + } + for (&site, (fns, _)) in &self.site_apply { + if fns.is_multi() || fns.ids().len() != 1 { + continue; + } + let Some(t) = fns.ids()[0].as_script() else { + continue; + }; + if hasown_scripts.contains(&t) { + facts + .apply_natives + .insert(site, crate::facts::ApplyNative::HasOwnProperty); + } + } + let mut apply_targets: HashMap = HashMap::default(); + for (&site, (fns, _)) in &self.site_apply { + if fns.is_multi() || fns.ids().len() != 1 { + continue; + } + let Some(target) = fns.ids()[0].as_script() else { + continue; + }; + apply_targets.insert(site, target); + } + facts.apply_targets = apply_targets.clone(); + // The per-context resolution, re-keyed by the site that entered + // the context: joined over every context minted at that site, and + // kept only where the join is still one script. + let enter_sites = self.ctxs.enter_sites(); + let mut by_entry: HashMap<(Site, Site), Agreed> = HashMap::default(); + for (&(cx, site), fns) in &self.site_apply_ctx { + let Some(entries) = enter_sites.get(&cx) else { + continue; + }; + let verdict = match fns.ids() { + [f] if !fns.is_multi() => f.as_script(), + _ => None, + }; + for &entry in entries { + let e = by_entry.entry((entry, site)).or_default(); + match verdict { + Some(t) => e.observe(t), + None => *e = Agreed::Conflict, + } + } + } + let mut sets: HashMap> = HashMap::default(); + for (&site, (fns, _)) in &self.site_apply { + if fns.is_multi() { + continue; + } + let e = sets.entry(site).or_default(); + e.extend(fns.ids().iter().filter_map(|f| f.as_script())); + } + for (key, a) in by_entry { + if let Some(&t) = a.get() { + facts.apply_targets_in.insert(key, t); + sets.entry(key.1).or_default().push(t); + } + } + for (site, mut v) in sets { + v.sort_unstable(); + v.dedup(); + facts.apply_target_sets.insert(site, v); + } + // Snapshot of the single-scripted-target sites, so the layout + // analysis can follow `this.m(...)` delegation without holding a + // borrow of `facts` while it fills the layout tables. + let single_call_target: HashMap = facts + .scripted_call_sites() + .filter_map(|(site, targets)| match targets { + [t] => Some((site, *t)), + _ => None, + }) + .collect(); + Delegation { + apply_targets, + single_call_target, + } + } + + // --- family 4: per-site heap facts ----------------------------------- + + /// Per property and element site: the key range, slot and claim it may + /// guard on, resolved against the layout plan. + fn emit_site_facts(&self, facts: &mut LikelyFacts, plan: &LayoutPlan) -> SiteFactTotals { + use super::engine::{CKey, Constraint}; + let mut totals = SiteFactTotals::default(); + // Write-site typed masks are deferred: they upgrade the store + // fence, which is pure cost unless some typed read consumes the + // position, and the reads are not all seen yet. + let mut typed_write_pending: Vec = Vec::new(); + for ci in 0..self.engine.cons.len() { + let (recv, name, pc, is_read) = match &self.engine.cons[ci] { + Constraint::Read { recv, name, pc, .. } => (*recv, *name, *pc, true), + Constraint::Write { recv, name, pc, .. } => (*recv, *name, *pc, false), + _ => continue, + }; + let sid = self.engine.con_script[ci]; + let site = Site::new(sid, pc); + // The value-CLASS tier, elems included: the agreed class of the + // loaded object, mapped through the same plan ranges the + // receiver rows use. Consumed as an ADVISORY likely-class on + // the result -- unchecked until a use guards it -- so + // recording it costs nothing at sites whose values never need + // identity. + if is_read { + if self.opts.diagnostics.propgap { + crate::diag_line!( + "night: valcls {site} name {} {:?} readts {:?}", + String::from_utf16_lossy(self.names.get(name)), + self.site_value_class.get(&site), + self.site_read_ts.get(&site).map(|t| t.obj) + ); + } + if let Some(&vc) = self.site_value_class.get(&site).and_then(Agreed::get) { + let range = if self.engine.region_root(vc) == vc + && self + .engine + .region_members + .get(&vc) + .is_some_and(|ms| ms.len() > 1) + { + plan.region_range.get(&vc).copied() + } else { + plan.range_of(self.group_of_class(vc)) + }; + if let Some((lo, hi)) = range { + facts + .field_cls_sites + .insert(site, (LayoutKey::new(lo), LayoutKey::new(hi))); + } + } + } + if name == self.names_of.elems { + self.emit_elem_site(facts, site, is_read, &mut totals); + continue; + } + if is_read { + // The full claim tier, not just the numeric one: a property + // read whose value is always an object gets the object-only + // claim, which the layout mask (a store-conformance claim, + // numeric by construction) cannot express. + if let Some(m) = self.site_read_ts.get(&site).and_then(TypeSet::site_claim) { + facts.field_sites.insert(site, m); + } + } + if plan.fill_add_sites.contains(&site) { + // Inside a fill run: the object is still at the prefix + // key, so the full-key fact would miss every time. + self.dump_prop_gap(site, name, is_read, "fill-run", recv); + continue; + } + let is_this_recv = recv == CKey::This; + if is_this_recv + && (facts.ctor_stamps.contains_key(&sid) + || facts.deleg_restamps.contains_key(&sid) + || facts.deleg_inits.contains(&sid)) + { + // Mid-construction: an idx-guarded arm can never hit. An + // init delegate's `this` is exactly as unstamped as a + // stamping ctor's -- its own return is what stamps it. + // Gated before BOTH resolution paths: the region tables + // serve this-typed sites too. + self.dump_prop_gap(site, name, is_read, "ctor-this", recv); + continue; + } + let Some(&c) = self.site_recv_class.get(&site).and_then(Agreed::get) else { + // This-layout precedence: an own-method `this` with a homed + // layout resolves against that key range even when the + // solver's receiver evidence stayed a region or conflict + // (a method's `this` cell can join a wider region than the + // method's own class). The emitted fact is key-range + // guarded, so a receiver outside the layout misses at + // runtime rather than misbehaving. + if is_this_recv { + if let Some(&(klo, khi)) = facts.this_layouts.get(&sid) { + let (lo, hi) = (klo.get(), khi.get()); + let table: &[NameId] = if lo != hi { + if let Some((pt, _)) = plan.group_tables.get(&lo) { + pt.as_slice() + } else { + plan.names(lo) + } + } else { + plan.names(lo) + }; + let table_masks = (lo != hi).then(|| { + plan.group_tables + .get(&lo) + .map_or(&[][..], |(_, mk)| mk.as_slice()) + }); + let found = plan + .table_slot_fact(lo, hi, table, table_masks, name) + .or_else(|| plan.subrange_in(lo, hi, name)); + if let Some(f) = found { + insert_prop_site(facts, site, f, is_read); + continue; + } + } + } + let had = facts.prop_sites.contains_key(&site); + self.emit_region_prop_site(facts, plan, site, name, is_read); + if !had && !facts.prop_sites.contains_key(&site) { + self.dump_prop_gap(site, name, is_read, "no-agreed-class", recv); + } + continue; + }; + // A receiver whose agreed class is a REGION ROOT stands for the + // whole region (an `AnyOf` receiver names its root), and the + // region's merged fact is the region table -- the universal + // prefix over every member's rows, guarded by the region's + // spanning key range. Resolving it against the root's own + // group instead guards on a range most members are outside of: + // a miss on every execution whose receiver is another member. + if self.engine.region_root(c) == c + && self + .engine + .region_members + .get(&c) + .is_some_and(|ms| ms.len() > 1) + { + let found = plan.region_range.get(&c).and_then(|&(rlo, rhi)| { + plan.region_tables + .get(&c) + .and_then(|(pt, pm)| plan.table_slot_fact(rlo, rhi, pt, Some(pm), name)) + .or_else(|| plan.subrange_in(rlo, rhi, name)) + }); + match found { + Some(f) => insert_prop_site(facts, site, f, is_read), + None => self.dump_prop_gap(site, name, is_read, "region-no-fact", recv), + } + continue; + } + let group = self.group_of_class(c); + let this_narrowed = if is_this_recv { + plan.narrow + .get(&sid) + .and_then(|cn| plan.ctor_key.get(cn)) + .copied() + } else { + None + }; + let (lo, hi, table): (u32, u32, Option<&[NameId]>) = + if let Some((kp, kf)) = this_narrowed { + // Two-phase pair: the prefix row is the pair's universal + // table (full-only names resolve through subrange_in to + // an exact full-key fact below). + (kp, kf, Some(plan.names(kp))) + } else { + let Some((lo, hi)) = plan.range_of(group) else { + self.dump_prop_gap(site, name, is_read, "no-key-range", recv); + continue; + }; + if lo == hi { + (lo, lo, Some(plan.names(lo))) + } else if let Some((pt, _)) = plan.group_tables.get(&lo) { + (lo, hi, Some(pt.as_slice())) + } else { + (lo, hi, None) + } + }; + // Name-keyed type-dimension mask: the class view cell's numeric + // claim for the accessed name, uniform across the site's key + // range -- independent of whether any slot fact exists (it + // covers post-init fields the layout row never named). Range + // sites merge member claims by union (the merged mask must + // cover every member's values); any member without a claim + // drops the site. + let nm: Option = if lo == hi { + self.class_view_prims(c, name) + } else { + (lo..=hi).try_fold(Prims::EMPTY, |acc, k| { + let c2 = plan.key_class.get(&k)?; + Some(acc | self.class_view_prims(*c2, name)?) + }) + }; + // Over a range the masks come from the group's own table, not + // from the layout the names came from -- and a two-phase pair + // takes its names from the prefix layout while its masks may + // have no table at all, in which case the range claims + // nothing. An exact key answers from its own layout instead. + let table_masks = (lo != hi).then(|| { + plan.group_tables + .get(&lo) + .map_or(&[][..], |(_, mk)| mk.as_slice()) + }); + let found = table + .and_then(|t| plan.table_slot_fact(lo, hi, t, table_masks, name)) + .or_else(|| { + let (glo, ghi) = plan.range_of(group)?; + plan.subrange_in(glo, ghi, name) + }); + if found.is_none() { + self.dump_prop_gap(site, name, is_read, "no-slot-fact", recv); + if self.opts.diagnostics.propgap { + let names: Vec = table + .map(|t| { + t.iter() + .map(|n| String::from_utf16_lossy(self.names.get(*n))) + .collect() + }) + .unwrap_or_default(); + crate::diag_line!( + "night: propgap-detail {site} keys {lo}..{hi} group-range {:?} table {}", + plan.range_of(group), + names.join(",") + ); + } + } + if let Some(f) = found { + insert_prop_site(facts, site, f, is_read); + } + if let Some(m) = nm { + // The typed row's range must equal the prop row's -- only + // then does a read consume (or a write maintain) the + // position. + let slot_pos = match found { + Some(f) if f.lo == lo && f.hi == hi => Some(f.slot), + _ => None, + }; + let row = (LayoutKey::new(lo), LayoutKey::new(hi), Claim::of_prims(m)); + if is_read { + facts.typed_sites.insert(site, row); + if let Some(s) = slot_pos { + for k in lo..=hi { + totals.typed_read_positions.insert((LayoutKey::new(k), s)); + } + } + } else { + typed_write_pending.push(PendingTypedWrite { + site, + row, + slot: slot_pos, + }); + } + } + } + // Consumer-driven store-claim admission: a write site's typed mask + // upgrades the fence only when some typed read consumes its (key, + // slot) position; an unconsumed claim is pure per-store cost, and a + // construction-heavy program pays it at every field init. + for w in typed_write_pending { + let consumed = w.slot.is_some_and(|s| { + (w.row.0.get()..=w.row.1.get()).any(|k| { + totals + .typed_read_positions + .contains(&(LayoutKey::new(k), s)) + }) + }); + if consumed { + facts.typed_sites.insert(w.site, w.row); + } + } + if !self.tables.saw_ta_ctor { + facts.elem_poly_sites.clear(); + } + // Fenced-claim subsumption: a site served by a prop_sites fact with + // a value claim rides the store fence, so the unfenced per-read + // value guard there is pure double-guarding -- a measurable loss in + // a hot method. The mask union upgrades prop_sites masks from + // typed_sites, so those sites are fenced too. field_sites survives + // only where no fenced table applies. + facts.field_sites.retain(|site, _| { + let prop = facts.prop_sites.get(site); + let fenced = matches!(prop, Some(&(_, _, _, m)) if m != Claim::NONE) + || (prop.is_some() + && matches!(facts.typed_sites.get(site), Some(&(_, _, m)) if m != Claim::NONE)); + !fenced + }); + totals + } + + /// One element site: the typed-array kind, the value claim, and the + /// array-claim cost/benefit tally. + fn emit_elem_site( + &self, + facts: &mut LikelyFacts, + site: Site, + is_read: bool, + totals: &mut SiteFactTotals, + ) { + let recv_root = self + .site_recv_class + .get(&site) + .and_then(Agreed::get) + .map(|&c| self.engine.region_root(c)) + .filter(|&r| self.heap[r].is_array); + if is_read { + if recv_root.is_some() { + totals.array_fold_reads += 1; + } + } else if recv_root.is_none() { + totals.array_unresolved_writes += 1; + } + if let Some(&tk) = self.site_recv_ta.get(&site).and_then(Agreed::get) { + facts.ta_elem_sites.insert(site, tk); + } + facts.elem_poly_sites.insert(site); + if is_read { + if let Some(m) = self + .site_read_ts + .get(&site) + .and_then(TypeSet::value_claim_full) + .filter(|m| !m.is_object()) + { + facts.elem_sites.insert(site, m); + } + } else if let Some(m) = self.write_site_elem_claim(site) { + facts.elem_write_sites.insert(site, m); + } + } + + /// The element claim of a WRITE site, read post-fixpoint: the join of + /// the `[]` view typesets of every region label the receiver agreed + /// on (a label without a view, or no agreement, yields nothing). + fn write_site_elem_claim(&self, site: Site) -> Option { + let labels = self.site_recv_labels.get(&site).and_then(AgreedSet::get)?; + let mut prims: Option = None; + for &c in labels { + let root = self.engine.region_root(c); + let cell = self + .engine + .existing_cell(crate::likelier::engine::CellKey::ClassView { + class: root, + name: self.names_of.elems, + })?; + let m = self.engine.ts(cell).value_claim_full()?; + if m.is_object() { + return None; + } + prims = Some(prims.map_or(m.prims(), |p| p | m.prims())); + } + prims.map(Claim::of_prims) + } + + /// The region rung: a property site whose receiver never resolved to + /// one class, but whose class labels all live in one region, gets the + /// region-range fact -- the same guard form over a wider range. + /// One record per property-access site the analysis leaves WITHOUT a + /// `prop_sites` row (`--dump-propgap`), naming the gate that refused. + /// + /// The class-fact arm is the compact property lowering; a site with no + /// row falls to the inline cache, which is the same ~540 bytes at every + /// one of them. The kill censuses say a fact died and `--dump-clsfact` + /// says whether a consumer wanted one; this says why the analysis never + /// made one, which is the only question the other two leave open. + fn dump_prop_gap( + &self, + site: Site, + name: NameId, + is_read: bool, + why: &str, + recv: super::engine::CKey, + ) { + use super::engine::CKey; + if !self.opts.diagnostics.propgap { + return; + } + // The receiver's own shape is most of the answer: a `Var` receiver + // is a value the body computed -- overwhelmingly a field or element + // read -- and there is no fact saying which class a field holds. + let r = match recv { + CKey::This => "this", + CKey::Arg(_) => "arg", + CKey::Var(_) => "var", + CKey::Ret => "ret", + CKey::GName(_) => "gname", + CKey::Aliased { .. } => "aliased", + }; + // The receiver's abstract object type is the whole story for + // `no-agreed-class`: `One`/`ClassAny` carry a class, `AnyOf` carries + // only a region label, `AnyObject` carries nothing and is the state + // a read off an unclassed receiver produces. + let k = match self.site_recv.get(&site) { + None => "unseen", + Some(super::RecvKind::Empty) => "Empty", + Some(super::RecvKind::One) => "One", + Some(super::RecvKind::ClassAny) => "ClassAny", + Some(super::RecvKind::AnyOf) => "AnyOf", + Some(super::RecvKind::AnyObject) => "AnyObject", + }; + crate::diag_line!( + "night: propgap {site} why {why} recv {r} objty {k} kind {} name {}", + if is_read { "get" } else { "set" }, + String::from_utf16_lossy(self.names.get(name)), + ); + } + + fn emit_region_prop_site( + &self, + facts: &mut LikelyFacts, + plan: &LayoutPlan, + site: Site, + name: NameId, + is_read: bool, + ) { + let Some(labels) = self.site_recv_labels.get(&site).and_then(AgreedSet::get) else { + if self.opts.diagnostics.propgap { + crate::diag_line!("night: propgap-region {site} no-labels"); + } + return; + }; + let mut it = labels.iter().map(|c| self.engine.region_root(*c)); + let Some(r0) = it.next() else { return }; + if !it.all(|r| r == r0) { + if self.opts.diagnostics.propgap { + crate::diag_line!("night: propgap-region {site} label-roots-disagree"); + } + return; + } + let found = plan.region_range.get(&r0).and_then(|&(rlo, rhi)| { + plan.region_tables + .get(&r0) + .and_then(|(pt, pm)| plan.table_slot_fact(rlo, rhi, pt, Some(pm), name)) + .or_else(|| plan.subrange_in(rlo, rhi, name)) + }); + if self.opts.diagnostics.propgap && found.is_none() { + crate::diag_line!( + "night: propgap-region {site} root cls{} range {:?} name {}", + r0.0, + plan.region_range.get(&r0), + String::from_utf16_lossy(self.names.get(name)) + ); + } + if let Some(f) = found { + // A fact whose key range excludes a class the site itself + // observed is self-contradicted: that population misses the + // guard on every execution. No fact beats a wrong one: the + // site keeps the IC, which serves everybody. + let excluded = labels.iter().any(|&c| { + plan.range_of(self.group_of_class(c)) + .is_some_and(|(glo, ghi)| ghi < f.lo || glo > f.hi) + }); + if excluded { + if self.opts.diagnostics.propgap { + crate::diag_line!( + "night: propgap-region {site} fact-excludes-seen-class name {}", + String::from_utf16_lossy(self.names.get(name)) + ); + } + return; + } + insert_prop_site(facts, site, f, is_read); + } + } + + /// The per-layout field rows the translator reads: name, write-tier + /// claim, value range, and the typed-tier claim. + fn emit_class_rows( + &self, + facts: &mut LikelyFacts, + plan: &LayoutPlan, + typed_read_positions: &HashSet<(LayoutKey, SlotIndex)>, + ) { + // Typed-tier layout claims: per layout position, the name-keyed + // claim where the wmask tier has none. Consumed only in + // fullword/dims mode as the store-fence + covered-read union. + let mut typed_prims_by_class: HashMap> = HashMap::default(); + for (&key, row) in &plan.rows { + let Some(&c) = plan.key_class.get(&key) else { + continue; + }; + let base = &row.masks; + let tm: Vec = row + .names + .iter() + .zip(base) + .enumerate() + .map(|(i, (name, &m0))| { + if !m0.is_empty() { + m0 + } else if typed_read_positions.contains(&( + LayoutKey::new(key), + SlotIndex::new(u32::try_from(i).unwrap()), + )) { + self.class_view_prims(c, *name) + .filter(|m| m.is_nonempty_subset_of(PRIM_INT32 | PRIM_DOUBLE)) + .unwrap_or(Prims::EMPTY) + } else { + Prims::EMPTY + } + }) + .collect(); + if tm.iter().zip(base).any(|(a, b)| a != b) { + typed_prims_by_class.insert(key, tm); + } + } + for (&k, row) in &plan.rows { + let prims = &row.masks; + let ranges = &row.ranges; + let typed = typed_prims_by_class.get(&k); + let fields = row + .names + .iter() + .enumerate() + .map(|(i, n)| ClassFieldFacts { + name: *n, + prims: prims.get(i).copied().unwrap_or(Prims::EMPTY), + range: ranges.get(i).copied().flatten(), + // An absent typed row means "same as the write tier", + // so the effective fullword claim is the write claim. + typed_prims: typed + .and_then(|t| t.get(i).copied()) + .unwrap_or_else(|| prims.get(i).copied().unwrap_or(Prims::EMPTY)), + }) + .collect(); + facts + .classes + .insert(LayoutKey::new(k), ClassFacts { fields }); + } + for (&lo, (pt, pm)) in &plan.group_tables { + facts + .group_tables + .insert(LayoutKey::new(lo), (pt.to_vec(), pm.clone())); + } + } + + /// Array element claims. A claim is keyed on the class-region root, + /// never a member site: `region_root` already merged the sites whose + /// arrays flowed together, and the root's cell holds their joined + /// evidence -- keying a member would let a tight sibling claim cover a + /// wilder population. + /// + /// Cost gate. An element write whose receiver did not resolve to a + /// single array population owes the maintenance duty unconditionally: + /// an element has no name to gate on, so unlike a field store it cannot + /// be shown irrelevant to every claim. That duty is paid at every such + /// site in the bundle, while the benefit accrues only at read sites a + /// claim can fold. Where the writes outnumber the folds the dimension + /// is pure tax, which is the shape of a large compiled-to-JS bundle: + /// one claiming population against thousands of unresolved element + /// writes. Static site counts only; no profile. + fn emit_array_claims(&self, facts: &mut LikelyFacts, totals: &SiteFactTotals) { + if totals.array_fold_reads <= totals.array_unresolved_writes { + return; + } + let n_elems = self.names_of.elems; + let mut roots: Vec = Vec::new(); + for (i, ci) in self.heap.classes.iter().enumerate() { + if !ci.is_array || ci.ta_kind.is_some() { + continue; + } + let c = ClassId(u32::try_from(i).unwrap()); + let root = self.engine.region_root(c); + if let ClassKey::Site(site) = ci.key { + facts + .array_alloc_sites + .insert(site, RegionRoot::new(root.0)); + } + if !roots.contains(&root) { + roots.push(root); + } + } + // Per-site receiver root, for both reads (the fold) and writes (the + // maintenance duty). Keyed by pc, so a field site on an array + // receiver can land here too -- harmless, the translator only + // consults this at element ops. + for (&site, c) in &self.site_recv_class { + if let Some(&c) = c.get() { + let root = self.engine.region_root(c); + if self.heap[root].is_array { + facts.array_elem_recv.insert(site, RegionRoot::new(root.0)); + } + } + } + for root in roots { + // The claim needs both: the range is the value's magnitude, the + // mask its tag, and the fold serves one int32 arm. + let (Some(m), Some(range)) = ( + self.class_view_prims(root, n_elems), + self.class_view_range(root, n_elems), + ) else { + continue; + }; + if m != PRIM_INT32 { + continue; + } + // A claim spanning all of int32 buys a consumer nothing -- an + // i32 operand already carries IV_I32 -- while still costing the + // fold and the store duty. A digit array whose hull is + // full-width lands exactly here: it needs a narrower range + // before the claim is worth anything. + if range.lo <= i64::from(i32::MIN) && range.hi >= i64::from(i32::MAX) { + continue; + } + facts + .array_elem_claims + .insert(RegionRoot::new(root.0), (m, range)); + } + } + + /// The predicted value range of a class's view cell, if bounded. The + /// interval component is already int32-clipped and quantized, so this is + /// just a projection; callers pair it with `class_view_prims` and drop + /// it wherever the mask claim is absent. + fn class_view_range(&self, c: ClassId, name: NameId) -> Option { + let cell = self + .engine + .lookup(super::engine::CellKey::ClassView { class: c, name })?; + match self.engine.ts(cell).interval { + super::types::Interval::In(r) => Some(r), + _ => None, + } + } + + /// The numeric mask of a class's view cell, if any. These masks ride + /// the store fence, so poison over numeric evidence gets the + /// optimistic int|double tier rather than a kill: + /// - unknown-only poison, same as the this-wmask path; + /// - an AnyObject obj part accompanied by unresolved evidence (the + /// unresolved-evidence typeset carries both -- poison, not object + /// evidence); + /// - null/undefined riding with numeric bits (the init-default / + /// reset idiom: `this.id = null` then ints forever; the fence + /// covers the resets per store). + /// + /// Definite string/bool/fn evidence, classed obj parts, and + /// objectness without the unknown bit stay honest kills: there the + /// whole-lifetime estimate says the claim would just decay. + /// + /// The claim is per-object ("SHALLOW set => claimed fields are + /// numbers"), the store fence clears SHALLOW on any non-conforming + /// store, and a wrong prediction costs the degrade path, never a deopt. + fn class_view_prims(&self, c: ClassId, name: NameId) -> Option { + let cell = self + .engine + .lookup(super::engine::CellKey::ClassView { class: c, name })?; + let ts = self.engine.ts(cell); + ts.pure_numeric().or_else(|| { + let base = ts.prims; + let unknown = ts.unknown; + let poisoned = unknown || base.intersects(PRIM_NULL | PRIM_UNDEFINED); + let obj_ok = match ts.obj { + ObjType::Empty => true, + ObjType::AnyObject => unknown, + _ => false, + }; + (poisoned + && base.intersects(PRIM_INT32 | PRIM_DOUBLE) + && base.subset_of(PRIM_INT32 | PRIM_DOUBLE | PRIM_NULL | PRIM_UNDEFINED) + && ts.fns.is_empty() + && obj_ok) + .then_some(if unknown { + // Unknown writes could add either numeric kind. + PRIM_INT32 | PRIM_DOUBLE + } else { + // Defaults-only poison: every numeric writer is in the + // cell, so the numeric projection is exact. Widening an + // int32-in-practice field to int|double from here costs + // real speed downstream -- the reads lose the int32 + // track. + base & (PRIM_INT32 | PRIM_DOUBLE) + }) + }) + } + + /// The class a script's `this` settled on, joined over live contexts. + fn script_this_class(&self, m: ScriptId) -> Option { + let ctxs = self.engine.live_ctxs.get(&m)?; + let mut found: Option = None; + for &cx in ctxs { + let Some(cell) = self + .engine + .lookup(super::engine::CellKey::This { script: m, ctx: cx }) + else { + continue; + }; + let c = match self.engine.ts(cell).obj { + ObjType::One(a) => self.heap.abs_class(a)?, + ObjType::ClassAny(c) => c, + ObjType::Empty => continue, + ObjType::AnyOf(_) | ObjType::AnyObject => return None, + }; + match found { + None => found = Some(c), + Some(prev) if prev != c => return None, + _ => {} + } + } + found + } +} diff --git a/js/src/night/compiler/src/likelier/engine.rs b/js/src/night/compiler/src/likelier/engine.rs new file mode 100644 index 0000000000000..4935bc6b5a478 --- /dev/null +++ b/js/src/night/compiler/src/likelier/engine.rs @@ -0,0 +1,1083 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The incremental fixpoint engine: cells, the constraint IR, subscriptions, +//! the worklist, and provenance. Constraints are generated once per script; +//! evaluation is per `(constraint, ctx)`. An edge re-fires whenever its +//! source cell grows. +//! +//! Determinism: all ids are dense and allocation-ordered; joins are +//! commutative on an immutable class labelling, so worklist order affects +//! only work, never the fixpoint. + +use super::types::{ + AbsId, AbsLabels, ClassId, CtxId, Interval, JoinSink, NameId, TypeSet, CTX0, MAX_CELL_CHANGES, +}; +use crate::facts::CallForm; +use crate::ids::{EnvSlot, FormalIndex, Pc, ScriptId, VarId}; +use crate::opsem::{Prims, ValueRange}; +use crate::source::SourceObjectId; +use rustc_hash::FxHashMap as HashMap; +use rustc_hash::FxHashSet as HashSet; +use std::collections::VecDeque; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct CellId(pub u32); + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct ConId(pub u32); + +/// Provenance sentinel for raises that come from initial state (snapshot +/// seeding, gname seeds) or standing links rather than a constraint. +pub const SEED: ConId = ConId(u32::MAX); + +/// Global cell identity. `Var`/`Arg`/`This`/`Ret` are per-context rows; +/// heap and global cells are context-free (context sensitivity lives in +/// which per-ctx rows have edges into them). +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum CellKey { + /// One local or temporary of a script, in one context. + Var { + script: ScriptId, + var: VarId, + ctx: CtxId, + }, + /// One formal position of a script, in one context. + Arg { + script: ScriptId, + arg: FormalIndex, + ctx: CtxId, + }, + /// A script's receiver, in one context. + This { + script: ScriptId, + ctx: CtxId, + }, + /// Everything a script returns, in one context. + Ret { + script: ScriptId, + ctx: CtxId, + }, + /// A global binding, by name. + GName(NameId), + /// One slot of a closure environment, named by the scope object that + /// owns it. Context-free: every closure over the scope shares the slot. + Aliased { + scope: SourceObjectId, + slot: EnvSlot, + }, + Field { + abs: AbsId, + name: NameId, + }, + ClassField { + class: ClassId, + name: NameId, + }, + ClassView { + class: ClassId, + name: NameId, + }, + /// Sentinel: raised (with a dummy bit) when the abstraction's proto + /// link is installed, so chain-walking reads that dead-ended re-fire. + ProtoSentinel(AbsId), + /// A script's accumulated `this.name = v` evidence, ctx-collapsed. + /// Standing links fan it into the ClassField cells of the script's + /// home classes (ctor class, single-home pin, this-forwarding + /// delegation), so this-attributed writes survive receiver saturation + /// -- the cell-graph form of the this_wmask side table's attribution. + ThisField { + script: ScriptId, + name: NameId, + }, + /// The bundle-wide union of every array abstraction's elems cell (fed + /// by standing links). Elems reads on AnyObject receivers consult it: + /// "an element read whose receiver we lost track of likely yields some + /// array's element" -- the guarded coarsening of unify-on-meet element + /// nodes. + ArrayElemsUnion, + /// Fn-table dispatch join row: the per-arg-index profile joined over + /// every dispatch site whose callee reads `abs`'s elems cell. Standing + /// links fan it into each snapshot member's Arg row at the generic + /// context -- one row, never per-target contexts (the set + /// is opaque at the sites, but the members' formals still learn the + /// join of what the table is called with). + TableArgJoin { + abs: AbsId, + arg: FormalIndex, + }, +} + +/// Context-relative cell reference inside a constraint. Constraint identity +/// is context-free; `(sid, ctx)` resolves a `CKey` to a `CellKey` at eval. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum CKey { + Var(VarId), + Arg(FormalIndex), + This, + Ret, + GName(NameId), + Aliased { + scope: SourceObjectId, + slot: EnvSlot, + }, +} + +#[derive(Clone, Debug)] +pub enum Constraint { + /// dst <- src. + Move { src: CKey, dst: CKey }, + /// dst <- fixed typeset (constants, unhandled-op Any, prim op results). + Const { dst: CKey, ts: TypeSet }, + /// dst <- receiver.name (heap semantics). `callee_pos`: the result + /// feeds a call's callee operand (set by the scanner); only such reads + /// consult the per-name method union on AnyObject receivers -- letting + /// union fns into generic value flow leaks them into escape sinks and + /// poisons the very method bodies the union serves. + Read { + recv: CKey, + name: NameId, + dst: CKey, + pc: Pc, + callee_pos: bool, + }, + /// receiver.name <- src (heap semantics). + Write { + recv: CKey, + name: NameId, + src: CKey, + pc: Pc, + }, + /// Call binding. `args` resolve against the caller's ctx + /// (Rc: constraints are cloned per eval; a Vec here allocated on the + /// solve hot path). + Call { + callee: CKey, + this_: Option, + args: std::rc::Rc<[CKey]>, + ret: CKey, + pc: Pc, + construct: bool, + }, + /// `T.apply(this, args)` / `T.call(this, a, b)`: a delegation call + /// edge. `args[0]` is the forwarded receiver; direct arguments are + /// `args[1..]` (call) or the unpacked frame `arguments` when + /// `arg1_is_arguments` (apply). + Apply { + target: CKey, + args: std::rc::Rc<[CKey]>, + arg1_is_arguments: bool, + ret: CKey, + pc: Pc, + form: CallForm, + }, + /// `recv.push/unshift(arg)` or `recv.pop/shift()`: the array builtins + /// that move a value in or out of the receiver's element node. + ElemBuiltin { + recv: CKey, + arg: Option, + ret: CKey, + pc: Pc, + kind: ElemBuiltinKind, + }, + /// Allocation site: dst <- One(Alloc(sid, pc, ctx)) (heap semantics). + /// `Snap` yields the ctx-free snapshot abstraction instead. + Alloc { dst: CKey, pc: Pc, kind: AllocKind }, + /// dst <- op(a[, b]): the operand-sensitive arithmetic transfer + /// (`types::arith_transfer`), which reads the operands' own claims + /// rather than returning one generic numeric mask. `a_lit`/`b_lit` are + /// exact scan-time literal intervals for the interval transfer (cells + /// hold only quantized bounds; the shift/mask rules need the literal). + Arith { + op: super::types::NumOp, + a: CKey, + b: Option, + dst: CKey, + a_lit: Option, + b_lit: Option, + pc: Pc, + }, +} + +/// Which kind of dataflow edge a constraint is. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ConstraintKind { + Move, + Const, + Read, + Write, + Call, + Apply, + ElemBuiltin, + Alloc, + Arith, +} + +impl ConstraintKind { + pub const ALL: [ConstraintKind; 9] = [ + ConstraintKind::Move, + ConstraintKind::Const, + ConstraintKind::Read, + ConstraintKind::Write, + ConstraintKind::Call, + ConstraintKind::Apply, + ConstraintKind::ElemBuiltin, + ConstraintKind::Alloc, + ConstraintKind::Arith, + ]; + + pub fn name(self) -> &'static str { + match self { + ConstraintKind::Move => "move", + ConstraintKind::Const => "const", + ConstraintKind::Read => "read", + ConstraintKind::Write => "write", + ConstraintKind::Call => "call", + ConstraintKind::Apply => "apply", + ConstraintKind::ElemBuiltin => "elem", + ConstraintKind::Alloc => "alloc", + ConstraintKind::Arith => "arith", + } + } +} + +impl Constraint { + pub fn kind(&self) -> ConstraintKind { + match self { + Constraint::Move { .. } => ConstraintKind::Move, + Constraint::Const { .. } => ConstraintKind::Const, + Constraint::Read { .. } => ConstraintKind::Read, + Constraint::Write { .. } => ConstraintKind::Write, + Constraint::Call { .. } => ConstraintKind::Call, + Constraint::Apply { .. } => ConstraintKind::Apply, + Constraint::ElemBuiltin { .. } => ConstraintKind::ElemBuiltin, + Constraint::Alloc { .. } => ConstraintKind::Alloc, + Constraint::Arith { .. } => ConstraintKind::Arith, + } + } +} + +/// Which array builtin an [`Constraint::ElemBuiltin`] edge models. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ElemBuiltinKind { + /// `push`/`unshift`: the argument flows into the receiver's elements. + Write, + /// `pop`/`shift`: an element flows out into the result. + Read, +} + +#[derive(Clone, Copy, Debug)] +pub enum AllocKind { + /// Object literal (`NewInit`/`NewObject`); the lit-order channel keys on + /// the site. + Plain, + /// Array literal, or `Array(n)` in either call form. + Array, + /// `new ()`, with the element kind the scanner read off + /// the constructor name. + TypedArray(crate::opsem::TaKind), + /// Precompiled run-once literal (`JSOp::Object`): the transcribed + /// source object it names, one ctx-free abstraction. + Snapshot(SourceObjectId), +} + +pub struct Cell { + pub key: CellKey, + pub ts: TypeSet, + changes: u16, +} + +/// The solver state: the cell graph, the constraints over it, and the +/// worklist that brings the two to a fixpoint. +/// +/// A *cell* holds the typeset of one program location (a local in a +/// context, a field of an abstraction, a global binding). A *constraint* +/// is one dataflow edge, generated once per script and evaluated once per +/// context the script is live at. Evaluating an edge reads its source +/// cells -- which subscribes it to them -- and raises its destination; a +/// raise that grows a cell re-fires that cell's subscribers. The fixpoint +/// is reached when the worklist drains. +#[derive(Default)] +pub struct Engine { + /// Every cell, indexed by [`CellId`]. + pub cells: Vec, + cell_ids: HashMap, + /// Every constraint, indexed by [`ConId`]. + pub cons: Vec, + /// The script each constraint was generated for, indexed by [`ConId`]: + /// the context-relative `CKey`s in a constraint resolve against it. + pub con_script: Vec, + /// The reverse index: a script's constraint ids, in emission order. + pub script_cons: HashMap>, + /// Contexts a script has been instantiated at (its live rows). + pub live_ctxs: HashMap>, + worklist: VecDeque<(ConId, CtxId)>, + inq: HashSet<(ConId, CtxId)>, + /// Dynamic subscriptions: cell -> (constraint, ctx) pairs to re-fire + /// when the cell grows. Installed on first read, permanent. + subs: HashMap>, + sub_set: HashSet<(CellId, ConId, CtxId)>, + /// Standing cell -> cell edges (`ClassView` feeds, proto plumbing): + /// a raise propagates through them immediately. + links: HashMap>, + link_set: HashSet<(CellId, CellId)>, + /// The join-visible labels of each abstraction, indexed by [`AbsId`] + /// and assigned at intern time by the heap layer; what makes joins + /// order-independent. + pub abs_labels: Vec, + /// Census counters: evaluations, raises. + pub n_evals: u64, + pub n_raises: u64, + /// Class-region union-find (parent map). A *region* is the set of + /// classes whose instances have been observed meeting; it is what a + /// meet of two differently-classed objects labels itself with instead + /// of collapsing to AnyObject. One union-find serves both kinds of + /// region (array and non-array) -- they differ in the meet rule, not + /// in the representation. Deterministic min-root; unions happen at + /// join time. + region_parent: HashMap, + /// Region root -> member classes (root included). Absent = singleton. + pub region_members: HashMap>, + /// Classes whose instances are arrays: the site classes minted for + /// plain-array allocations. Membership decides which meet rule two + /// classed objects take (see `join_ts`). + pub array_classes: HashSet, + /// The interned elems name (set once by the solver; view links need it). + pub elems_name: Option, + /// AnyObject-transition attribution: constraint -> number of cells it + /// flipped to AnyObject (the design's failure mode, so its provenance + /// is a first-class census). + pub anyobj_why: HashMap, + /// The first transitions, in order (genesis vs cascade). + pub anyobj_first: Vec<(CellKey, ConId)>, + /// Join diagnostics (One+One absorption pairs, dropped fn ids); + /// censused only -- nothing consumes them. + pub sink: JoinSink, +} + +impl Engine { + /// The cell for `key` if one was ever created (no allocation). + pub fn existing_cell(&self, key: CellKey) -> Option { + self.cell_ids.get(&key).copied() + } + + pub fn cell(&mut self, key: CellKey) -> CellId { + if let Some(&id) = self.cell_ids.get(&key) { + return id; + } + let id = CellId(u32::try_from(self.cells.len()).unwrap()); + self.cells.push(Cell { + key, + ts: TypeSet::default(), + changes: 0, + }); + self.cell_ids.insert(key, id); + id + } + + pub fn lookup(&self, key: CellKey) -> Option { + self.cell_ids.get(&key).copied() + } + + /// Every class-view (field) cell, for the viz. + pub fn field_cells(&self) -> Vec<(ClassId, crate::ids::NameId, CellId)> { + let mut out: Vec<_> = self + .cell_ids + .iter() + .filter_map(|(k, &id)| match k { + CellKey::ClassView { class, name } => Some((*class, *name, id)), + _ => None, + }) + .collect(); + out.sort_by_key(|(c, n, _)| (c.0, n.0)); + out + } + + /// Every gname cell (context-free by construction), for the fact + /// emitter's projection pass. + pub fn gname_cells(&self) -> Vec<(crate::ids::NameId, CellId)> { + let mut out: Vec<_> = self + .cell_ids + .iter() + .filter_map(|(k, &id)| match k { + CellKey::GName(n) => Some((*n, id)), + _ => None, + }) + .collect(); + out.sort_by_key(|&(n, _)| n); + out + } + + pub fn ts(&self, id: CellId) -> &TypeSet { + &self.cells[id.0 as usize].ts + } + + pub fn add_con(&mut self, script: ScriptId, con: Constraint) -> ConId { + let id = ConId(u32::try_from(self.cons.len()).unwrap()); + self.cons.push(con); + self.con_script.push(script); + self.script_cons.entry(script).or_default().push(id); + id + } + + /// Enqueue every constraint of `script` at `ctx` (first time only). + pub fn instantiate(&mut self, script: ScriptId, ctx: CtxId) -> bool { + let ctxs = self.live_ctxs.entry(script).or_default(); + if ctxs.contains(&ctx) { + return false; + } + ctxs.push(ctx); + if let Some(cons) = self.script_cons.get(&script) { + for &c in cons.clone().iter() { + self.enqueue(c, ctx); + } + } + true + } + + /// Resolve a constraint-relative cell reference against the script and + /// context it is being evaluated at. + pub fn resolve(&mut self, script: ScriptId, ctx: CtxId, k: CKey) -> CellId { + let key = match k { + CKey::Var(var) => CellKey::Var { script, var, ctx }, + CKey::Arg(arg) => CellKey::Arg { script, arg, ctx }, + CKey::This => CellKey::This { script, ctx }, + CKey::Ret => CellKey::Ret { script, ctx }, + CKey::GName(name) => CellKey::GName(name), + CKey::Aliased { scope, slot } => CellKey::Aliased { scope, slot }, + }; + self.cell(key) + } + + /// Read a cell on behalf of `(con, ctx)`: subscribes the reader (so it + /// re-fires when the source grows) and returns a snapshot. + pub fn read(&mut self, id: CellId, user: (ConId, CtxId)) -> TypeSet { + if self.sub_set.insert((id, user.0, user.1)) { + self.subs.entry(id).or_default().push(user); + } + self.cells[id.0 as usize].ts.clone() + } + + /// The region root of a class: the representative of every class this + /// one has met with. A class that has never met another is its own + /// root. + pub fn region_root(&self, c: ClassId) -> ClassId { + let mut cur = c; + while let Some(&p) = self.region_parent.get(&cur) { + if p == cur { + break; + } + cur = p; + } + cur + } + + /// Merge two classes' regions (min root wins, deterministically) and + /// chain the loser's elems view into the winner's, so a read through + /// the merged root sees both populations' elements. Merging a class + /// with itself, or with a class already in its region, is just + /// [`Engine::region_root`] -- the early return below is what lets + /// callers hand it any pair. + fn union_regions(&mut self, c: ClassId, d: ClassId) -> ClassId { + let rc = self.region_root(c); + let rd = self.region_root(d); + if rc == rd { + return rc; + } + let (win, lose) = if rc < rd { (rc, rd) } else { (rd, rc) }; + self.region_parent.insert(lose, win); + let lm = self + .region_members + .remove(&lose) + .unwrap_or_else(|| vec![lose]); + self.region_members + .entry(win) + .or_insert_with(|| vec![win]) + .extend(lm); + if let Some(en) = self.elems_name { + let from = self.cell(CellKey::ClassView { + class: lose, + name: en, + }); + let to = self.cell(CellKey::ClassView { + class: win, + name: en, + }); + self.link(from, to); + } + win + } + + /// The class of an object part whose instances are arrays, when it has + /// one. `None` for a non-array part, a part with no class at all, and + /// for `AnyOf` -- a non-array region by construction, since the meet + /// rule below never puts an array class in one. + fn array_region_of(&self, o: super::types::ObjType) -> Option { + use super::types::ObjType::*; + match o { + One(a) => { + let m = self.abs_labels.get(a.0 as usize)?; + if m.array { + m.class + .filter(|c| self.array_classes.contains(&self.region_root(*c))) + } else { + None + } + } + ClassAny(c) => { + if self.array_classes.contains(&self.region_root(c)) { + Some(c) + } else { + None + } + } + _ => None, + } + } + + /// The class (or, for `AnyOf`, the region root) an object part carries, + /// plus whether its instances are arrays. The `AnyOf` arm answers + /// `false` unconditionally because the meet rule below only ever forms + /// an `AnyOf` region out of non-array classes. + fn class_and_arrayness(&self, o: super::types::ObjType) -> (Option, bool) { + use super::types::ObjType::*; + match o { + One(a) => match self.abs_labels.get(a.0 as usize) { + Some(m) => (m.class, m.array), + None => (None, false), + }, + ClassAny(c) => (Some(c), self.array_classes.contains(&self.region_root(c))), + AnyOf(r) => (Some(r), false), + _ => (None, false), + } + } + + /// Engine-aware typeset join: `TypeSet::join_from` plus the two cases + /// where two classed object parts, instead of collapsing to AnyObject, + /// merge their classes into one region and meet as that region. + /// + /// Arrays are not a separate world in the model -- an array's elements + /// live in an ordinary field cell under the reserved `ELEMS` name, and + /// arrayness is one bit on the class. What is array-specific is only + /// this meet rule, in three cases: + /// + /// - array meets array -> `ClassAny(root)`. The merged root keeps a + /// class, so reads still go through the element view cell of that + /// population. + /// - non-array meets non-array -> `AnyOf(root)`, the weaker label: a + /// read through it yields unresolved evidence (plus, at callee + /// position, the region's method-table union). + /// - array meets non-array -> `AnyObject`. Merging the two would put + /// every array population into the same region as every object + /// population it ever met. + /// + /// Collapsing all three cases into the `AnyOf` rule would land every + /// array population of a program in one region, so + /// `array_alloc_sites`/`array_elem_recv`/`typed_sites` could no + /// longer tell them apart. + pub fn join_ts(&mut self, dst: &mut TypeSet, src: &TypeSet) -> bool { + use super::types::ObjType; + let jo = match (self.array_region_of(dst.obj), self.array_region_of(src.obj)) { + (Some(c), Some(d)) => Some(ObjType::ClassAny(self.union_regions(c, d))), + _ => { + let pure = super::types::join_obj(dst.obj, src.obj, &self.abs_labels); + if pure == ObjType::AnyObject { + let (ca, aa) = self.class_and_arrayness(dst.obj); + let (cb, ab) = self.class_and_arrayness(src.obj); + match (ca, cb) { + (Some(c), Some(d)) if !aa && !ab => { + Some(ObjType::AnyOf(self.union_regions(c, d))) + } + _ => None, + } + } else { + None + } + } + }; + let changed = dst.join_from(src, &self.abs_labels, &mut self.sink); + match jo { + Some(j) if dst.obj != j => { + dst.obj = j; + true + } + Some(_) => changed, + None => changed, + } + } + + /// Join one cell across every context a script was live at. + /// + /// Deliberately not [`Engine::join_ts`]: this is the projection the + /// emission and the trace use, and it must not merge class regions. + /// `join_ts` unions the region union-find as a side effect, which is + /// right while the fixpoint is running and wrong afterwards -- reading + /// out an answer should not change it. So the object part here keeps + /// the last non-empty label rather than meeting the labels, and the + /// consumers only ask coarse questions of it ("was there an object at + /// all", "which single class"). + /// + /// Every other component joins exactly as `TypeSet::join_from` does, + /// the object part being the single deliberate difference. + pub fn join_over_ctxs(&self, ctxs: &[CtxId], mk: impl Fn(CtxId) -> CellKey) -> Option { + let mut joined: Option = None; + for &ctx in ctxs { + let Some(cid) = self.lookup(mk(ctx)) else { + continue; + }; + let ts = self.ts(cid).clone(); + match &mut joined { + None => joined = Some(ts), + Some(j) => { + j.prims |= ts.prims; + j.unknown |= ts.unknown; + if ts.range > j.range { + j.range = ts.range; + } + if ts.obj != super::types::ObjType::Empty { + j.obj = ts.obj; + } + if !ts.fns.is_empty() { + let f = ts.fns.clone(); + j.fns.join_from(&f, &mut Vec::new()); + } + j.interval = Interval::join(j.interval, ts.interval); + } + } + } + joined + } + + /// Monotone raise; on growth, re-fires the cell's subscribers. + pub fn raise(&mut self, id: CellId, ts: &TypeSet, why: (ConId, CtxId)) { + self.trace_cell(id, ts, why); + { + // No-growth fast path: cells change O(1) times but are raised + // constantly, and the clone + engine join per raise is + // allocation-heavy. Conservatively limited to raises that + // cannot trigger a region merge or relabel: prim/fn/range/ + // interval subset with an Empty or identical non-array obj + // part. + let cur = &self.cells[id.0 as usize].ts; + if ts.prims | cur.prims == cur.prims + && ts.fns.is_subset_of(&cur.fns) + && (ts.obj == super::types::ObjType::Empty + || (ts.obj == cur.obj && self.array_region_of(ts.obj).is_none())) + && ts.range <= cur.range + && cur.interval.subsumes(ts.interval) + { + return; + } + } + let was_anyobj = self.cells[id.0 as usize].ts.obj == super::types::ObjType::AnyObject; + let mut joined = self.cells[id.0 as usize].ts.clone(); + if !self.join_ts(&mut joined, ts) { + return; + } + self.cells[id.0 as usize].ts = joined; + let cell = &mut self.cells[id.0 as usize]; + let flipped_anyobj = !was_anyobj && cell.ts.obj == super::types::ObjType::AnyObject; + let key = cell.key; + if flipped_anyobj { + *self.anyobj_why.entry(why.0).or_insert(0) += 1; + if self.anyobj_first.len() < 12 { + self.anyobj_first.push((key, why.0)); + } + } + cell.changes += 1; + debug_assert!( + cell.changes <= MAX_CELL_CHANGES, + "cell {:?} exceeded the lattice-height change bound", + cell.key + ); + self.n_raises += 1; + if let Some(users) = self.subs.get(&id) { + for &(c, ctx) in users { + if self.inq.insert((c, ctx)) { + self.worklist.push_back((c, ctx)); + } + } + } + if let Some(dsts) = self.links.get(&id) { + let v = self.cells[id.0 as usize].ts.clone(); + for d in dsts.clone() { + self.raise(d, &v, why); + } + } + } + + /// Install a standing `src -> dst` edge and propagate the current value + /// (idempotent). Termination through link cycles comes from `raise` + /// stopping at no-change. + pub fn link(&mut self, src: CellId, dst: CellId) { + if src == dst || !self.link_set.insert((src, dst)) { + return; + } + self.links.entry(src).or_default().push(dst); + let v = self.cells[src.0 as usize].ts.clone(); + if !v.is_empty() { + self.raise(dst, &v, (SEED, CTX0)); + } + } + + /// Debug tracer for one cell (`--trace-cell arg::` or + /// `local::`): every raise into it, with the incoming object + /// type and the constraint responsible. Answers "which writer made this + /// slot AnyObject", which no census can, because the answer is a single + /// join step inside the solver. + fn trace_cell(&self, id: CellId, ts: &TypeSet, why: (ConId, CtxId)) { + let Some(want) = super::tracers().cell.as_ref() else { + return; + }; + let key = self.cells[id.0 as usize].key; + let got = match key { + CellKey::Arg { script, arg, .. } => format!("arg:{}:{}", script.get(), arg.get()), + CellKey::Var { script, var, .. } => format!("local:{}:{}", script.get(), var.get()), + _ => return, + }; + if got != *want { + return; + } + let src = self.con_script.get(why.0 .0 as usize).copied(); + let con = self + .cons + .get(why.0 .0 as usize) + .map_or_else(|| "?".to_string(), |c| format!("{c:?}")); + crate::diag_line!( + "night: tracecell {got} <- obj {:?} unknown {} from sid {:?} con {}", + ts.obj, + u8::from(ts.unknown), + src.map(|s| s.get()), + con + ); + } + + pub fn enqueue(&mut self, con: ConId, ctx: CtxId) { + if self.inq.insert((con, ctx)) { + self.worklist.push_back((con, ctx)); + } + } + + pub fn pop(&mut self) -> Option<(ConId, CtxId)> { + let item = self.worklist.pop_front()?; + self.inq.remove(&item); + self.n_evals += 1; + Some(item) + } + + /// Core structural constraints (`Move`/`Const`). Heap and call + /// constraints are dispatched by the solver layers above. + pub fn eval_core(&mut self, con: ConId, ctx: CtxId) -> bool { + let sid = self.con_script[con.0 as usize]; + match self.cons[con.0 as usize].clone() { + Constraint::Move { src, dst } => { + let s = self.resolve(sid, ctx, src); + let d = self.resolve(sid, ctx, dst); + let v = self.read(s, (con, ctx)); + self.raise(d, &v, (con, ctx)); + true + } + Constraint::Const { dst, ts } => { + let d = self.resolve(sid, ctx, dst); + self.raise(d, &ts, (con, ctx)); + true + } + Constraint::Arith { + op, + a, + b, + dst, + a_lit, + b_lit, + pc: _, + } => { + let ca = self.resolve(sid, ctx, a); + let ta = self.read(ca, (con, ctx)); + let tb = b.map(|k| { + let cb = self.resolve(sid, ctx, k); + self.read(cb, (con, ctx)) + }); + let (m, r) = super::types::arith_transfer(op, &ta, tb.as_ref()); + let interval = super::types::arith_interval( + op, + super::types::operand_interval(&ta), + a_lit, + tb.as_ref().map(super::types::operand_interval), + b_lit, + ); + // The interval component flows even when the mask side is + // empty (mask-invisible shadow operands carry interval only). + if m != Prims::EMPTY || interval != Interval::Empty { + let d = self.resolve(sid, ctx, dst); + self.raise(d, &TypeSet::prim_interval(m, r, interval), (con, ctx)); + } + true + } + _ => false, + } + } +} + +/// Drive `eval` to fixpoint. `eval` must fully handle every constraint kind +/// present (the solver composes `eval_core` with heap/call evaluation). +pub fn run(engine: &mut Engine, mut eval: impl FnMut(&mut Engine, ConId, CtxId)) { + while let Some((c, ctx)) = engine.pop() { + eval(engine, c, ctx); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const S1: ScriptId = ScriptId::new(1); + const S2: ScriptId = ScriptId::new(2); + use crate::likelier::types::{BoundedFnSet, FnId, ObjType}; + use crate::opsem::{PRIM_DOUBLE, PRIM_INT32}; + + fn core_solver(e: &mut Engine) { + run(e, |e, c, ctx| { + let handled = e.eval_core(c, ctx); + assert!(handled, "test graphs use only core constraints"); + }); + } + + /// A diamond with a cycle: v0 -> v1 -> v2 -> v1 (loop), v2 -> v3. + fn build_cycle(e: &mut Engine) { + e.add_con( + S1, + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::prim(PRIM_INT32), + }, + ); + e.add_con( + S1, + Constraint::Move { + src: CKey::Var(VarId::new(0)), + dst: CKey::Var(VarId::new(1)), + }, + ); + e.add_con( + S1, + Constraint::Move { + src: CKey::Var(VarId::new(1)), + dst: CKey::Var(VarId::new(2)), + }, + ); + e.add_con( + S1, + Constraint::Move { + src: CKey::Var(VarId::new(2)), + dst: CKey::Var(VarId::new(1)), + }, + ); + e.add_con( + S1, + Constraint::Move { + src: CKey::Var(VarId::new(2)), + dst: CKey::Var(VarId::new(3)), + }, + ); + e.add_con( + S1, + Constraint::Const { + dst: CKey::Var(VarId::new(2)), + ts: TypeSet::prim(PRIM_DOUBLE), + }, + ); + } + + #[test] + fn fixpoint_through_cycle() { + let mut e = Engine::default(); + build_cycle(&mut e); + e.instantiate(S1, CTX0); + core_solver(&mut e); + let v3 = e + .lookup(CellKey::Var { + script: S1, + var: VarId::new(3), + ctx: CTX0, + }) + .unwrap(); + assert_eq!(e.ts(v3).prims, PRIM_INT32 | PRIM_DOUBLE); + // The back edge propagated the loop-added double into v1 too. + let v1 = e + .lookup(CellKey::Var { + script: S1, + var: VarId::new(1), + ctx: CTX0, + }) + .unwrap(); + assert_eq!(e.ts(v1).prims, PRIM_INT32 | PRIM_DOUBLE); + } + + #[test] + fn refire_on_late_source_growth() { + // A reader that evaluates before its source is written must re-fire: + // the exact failure mode of one-way edges without a fixpoint. + let mut e = Engine::default(); + // Script 1 reads gname g into v0 (evaluates first). + let mut names = crate::likelier::types::Names::default(); + let g = names.intern(&[103]); + e.add_con( + S1, + Constraint::Move { + src: CKey::GName(g), + dst: CKey::Var(VarId::new(0)), + }, + ); + // Script 2 writes g (instantiated after script 1 has quiesced). + e.instantiate(S1, CTX0); + core_solver(&mut e); + e.add_con( + S2, + Constraint::Const { + dst: CKey::GName(g), + ts: TypeSet::fn_one(FnId::script(ScriptId::new(42))), + }, + ); + e.instantiate(S2, CTX0); + core_solver(&mut e); + let v0 = e + .lookup(CellKey::Var { + script: S1, + var: VarId::new(0), + ctx: CTX0, + }) + .unwrap(); + assert_eq!( + e.ts(v0).fns, + BoundedFnSet::one(FnId::script(ScriptId::new(42))) + ); + } + + #[test] + fn per_ctx_rows_are_distinct() { + let mut e = Engine::default(); + e.add_con( + S1, + Constraint::Move { + src: CKey::Arg(FormalIndex::new(0)), + dst: CKey::Ret, + }, + ); + let ctx1 = CtxId(1); + e.instantiate(S1, CTX0); + e.instantiate(S1, ctx1); + let a0 = e.cell(CellKey::Arg { + script: S1, + arg: FormalIndex::new(0), + ctx: CTX0, + }); + let a1 = e.cell(CellKey::Arg { + script: S1, + arg: FormalIndex::new(0), + ctx: ctx1, + }); + e.raise(a0, &TypeSet::prim(PRIM_INT32), (ConId(0), CTX0)); + e.raise(a1, &TypeSet::prim(PRIM_DOUBLE), (ConId(0), ctx1)); + core_solver(&mut e); + let r0 = e + .lookup(CellKey::Ret { + script: S1, + ctx: CTX0, + }) + .unwrap(); + let r1 = e + .lookup(CellKey::Ret { + script: S1, + ctx: ctx1, + }) + .unwrap(); + assert_eq!(e.ts(r0).prims, PRIM_INT32); + assert_eq!(e.ts(r1).prims, PRIM_DOUBLE); + } + + #[test] + #[allow(clippy::field_reassign_with_default)] + fn one_plus_one_meets_as_class_any() { + let mut e = Engine::default(); + // classes: abs 0,1 -> class 0 + e.abs_labels = vec![ + AbsLabels { + class: Some(ClassId(0)), + snap: false, + array: false, + }, + AbsLabels { + class: Some(ClassId(0)), + snap: false, + array: false, + }, + AbsLabels::default(), + ]; + build_cycle(&mut e); + e.add_con( + S1, + Constraint::Const { + dst: CKey::Var(VarId::new(1)), + ts: TypeSet::obj_one(AbsId(0)), + }, + ); + e.add_con( + S1, + Constraint::Const { + dst: CKey::Var(VarId::new(2)), + ts: TypeSet::obj_one(AbsId(1)), + }, + ); + e.instantiate(S1, CTX0); + core_solver(&mut e); + let v1 = e + .lookup(CellKey::Var { + script: S1, + var: VarId::new(1), + ctx: CTX0, + }) + .unwrap(); + assert_eq!(e.ts(v1).obj, ObjType::ClassAny(ClassId(0))); + } + + #[test] + fn work_bound_scales_with_edges() { + // A long chain: total evaluations must stay O(edges), not O(n^2). + let mut e = Engine::default(); + let n = 2000u32; + e.add_con( + S1, + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::prim(PRIM_INT32), + }, + ); + for i in 0..n { + e.add_con( + S1, + Constraint::Move { + src: CKey::Var(VarId::new(i)), + dst: CKey::Var(VarId::new(i + 1)), + }, + ); + } + e.instantiate(S1, CTX0); + core_solver(&mut e); + let last = e + .lookup(CellKey::Var { + script: S1, + var: VarId::new(n), + ctx: CTX0, + }) + .unwrap(); + assert_eq!(e.ts(last).prims, PRIM_INT32); + // Each edge fires at initial instantiation plus at most + // MAX_CELL_CHANGES re-fires from its source growing. + assert!( + e.n_evals <= (u64::from(n) + 1) * (u64::from(MAX_CELL_CHANGES as u32) + 1), + "evals {} exceed the lattice work bound", + e.n_evals + ); + } +} diff --git a/js/src/night/compiler/src/likelier/heap.rs b/js/src/night/compiler/src/likelier/heap.rs new file mode 100644 index 0000000000000..c64802c5c26e8 --- /dev/null +++ b/js/src/night/compiler/src/likelier/heap.rs @@ -0,0 +1,2754 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The heap model: abstractions, field cells, classes, prototype chains, +//! and snapshot seeding as initial state (no oracle, no consultation points +//! -- program writes join into the same cells the snapshot pre-filled). +//! +//! Structure rules (all monotone, and all order-independent -- see the +//! note on prototype installs below): +//! - Two distinct snapshot objects are two distinct abstractions; their join +//! is ClassAny/AnyObject. Nothing can fuse their field cells. +//! - A class's method table is the field space of its proto abstraction +//! (`ProtoOf(class)`, synthetic); concrete prototype objects registered as +//! sources feed it through standing per-name links. A proto link is a +//! lookup edge, never value flow: `B.prototype = new A()` must not make +//! A's instance fields flow into B's. +//! - `ClassView(C, name)` is the read view for ClassAny receivers: every +//! `Field(a, name)` with `class(a) = C` and `ClassField(C, name)` feed it. +//! A One(a) read reads only its own cell + ClassField + chain: letting it +//! see sibling instances' values would merge a whole class into one cell, +//! which is the precision this model exists to keep. +//! - A named property read does walk the prototype chain, and it joins +//! every level rather than stopping at one (`chain_join`). The +//! interpreter stops at the first object that has the property; the +//! analysis cannot tell which object that will be, so picking a level +//! would be a guess, and the alternative -- an if-else over "does this +//! level have it" -- is not something a cell can express, since a cell +//! holds what may flow there, not whether the property exists. Joining +//! the levels answers the question the cells can answer: what values +//! this read may see. That is how a method read off an instance finds +//! its class's method table, which lives one level up. +//! Element reads (the reserved `ELEMS` name) are the exception -- they +//! consult the receiver's own cell only, since joining up the chain +//! would pour every array's elements into one shared cell, and no real +//! program inherits its elements. +//! +//! Determinism and prototype installs: the structure above is built as +//! constraints evaluate, so the *order* in which two `F.prototype = ...` +//! installs are seen decides which one wins the class's upward chain link +//! and which method scripts get homed to which class (`register_proto_source`, +//! `note_method_home`: first install wins, a differing second install +//! demotes to none). That is not a source of run-to-run nondeterminism -- +//! the worklist order is itself deterministic, so the same input yields +//! the same answer every time -- but it does mean a program that installs +//! two different prototypes on one constructor is answered by whichever +//! install the fixpoint reaches first, rather than by a join of the two. +//! Field *values* have no such rule: they always join. + +use super::builtins::{self, NativeKind}; +use super::engine::{AllocKind, CellId, CellKey, ConId, Constraint, ElemBuiltinKind, SEED}; +use super::types::{observe, Agreed}; +use super::types::{AbsId, AbsLabels, ClassId, CtxId, FnId, NameId, ObjType, TypeSet, CTX0}; +use super::{RecvKind, SharedCtorSite, Solver}; +use crate::constants::{CHAIN_DEPTH, MAX_HOMES, RECV_LABEL_CAP}; +use crate::ids::{EnvSlot, FormalIndex, JsString, Pc, ScriptId, Site, VarId}; +use crate::opsem::{ + Prims, TaKind, PRIM_BOOLEAN, PRIM_DOUBLE, PRIM_INT32, PRIM_NULL, PRIM_STRING, PRIM_SYMBOL, + PRIM_UNDEFINED, +}; +use crate::source::{ + ObjectData, ObjectKind, Primitive, ScopeData, Source, SourceObject, SourceObjectId, +}; +use rustc_hash::FxHashMap as HashMap; +use rustc_hash::FxHashSet as HashSet; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum AbsKey { + /// A transcribed snapshot object (including the global). + Snap(SourceObjectId), + /// A script's function-object statics space (`F.staticName`). + FnObj(ScriptId), + /// The synthetic prototype abstraction of a class: its field space IS + /// the method table. + ProtoOf(ClassId), + /// An allocation site, per context. + Alloc { + script: ScriptId, + pc: Pc, + ctx: CtxId, + }, + /// A synthesized builtin namespace (Math, json, ...): the walker + /// cannot transcribe unregistered native objects, so their method + /// tables are seeded from the spec (index into namespaces). + NativeNs(u8), +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ProtoLink { + Abs(AbsId), + None, +} + +pub struct Abstraction { + pub key: AbsKey, + /// Immutable class label, assigned at intern time (what makes typeset + /// joins order-independent). + pub class: Option, + pub proto: ProtoLink, + /// The class whose method table this abstraction feeds (proto objects); + /// method-home attribution keys on it. + pub owner_class: Option, + /// `ProtoOf` back-pointer. + pub proto_of: Option, + /// The element kind, when this abstraction is a typed array. + pub ta_kind: Option, + /// Whether this abstraction's object is a JS Array (a dense + /// integer-indexed exotic object), as opposed to any object that + /// merely happens to carry integer-keyed properties. Both kinds put + /// their elements in the same place -- the field cell of the reserved + /// `ELEMS` name -- so this bit does not decide where elements live. It + /// decides the meet rule (`Engine::join_ts`: two array populations + /// merge into a region that keeps its element view, an array and a + /// non-array collapse to AnyObject) and the array-claim emission. + pub is_array: bool, + /// Whether the transcribed snapshot object's properties have been + /// copied into this abstraction's field cells yet (`ensure_seeded`). + /// Seeding is lazy -- a bundle has far more snapshot objects than the + /// program ever touches -- and happens exactly once. + seeded: bool, +} + +/// Snapshot-confirmed class identity: the concrete `.prototype` cell when +/// one exists, else the constructor script (the lazy-`Function.prototype` +/// trap: a pure data record has no prototype object). +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum ClassKey { + Proto(SourceObjectId), + Script(ScriptId), + /// Per-allocation-site pseudo-class for classless literals/arrays, so + /// same-site cross-context abstractions join to ClassAny(site) instead + /// of AnyObject. + Site(Site), +} + +pub struct ClassInfo { + pub key: ClassKey, + pub ctor: Option, + pub proto_abs: AbsId, + /// Concrete prototype objects feeding the method table. + pub sources: Vec, + /// Site classes only: the allocation's typed-array element kind. + pub ta_kind: Option, + /// Site classes only: whether the allocation makes a JS Array. See + /// [`Abstraction::is_array`]. + pub is_array: bool, +} + +#[derive(Default)] +pub struct Heap { + abs_ids: HashMap, + pub abs: Vec, + class_ids: HashMap, + pub classes: Vec, + /// Field names ever interned per abstraction (drives late source + /// registration). + fields_of: HashMap>, + /// Script -> the prototype object shared by all its live closures. + pub script_proto: HashMap, + /// `.prototype` object id -> constructor script (the concrete->class + /// bridge). + pub proto_owner: HashMap, + /// Script -> its transcribed snapshot function objects, for seeding + /// the `FnObj` statics space (`F.staticName` installed at wizen time + /// lives only in the snapshot object). + script_fn_objs: HashMap>, + /// Method script -> the constructor whose method table installed it, + /// while every install agrees. + pub method_home: HashMap>, + /// Literal sites whose allocation became a prototype object (their + /// "fields" are a method table, not instance layout evidence). + pub site_is_proto: HashSet, + /// Allocation sites whose objects receive computed-name property + /// writes (a for-in copy like `Object.extend`): the generic store path + /// cannot certify slot conformance, so SLOTS never holds on these + /// objects and a slot row would arm a guard that misses forever. + pub dyn_named_writes: HashSet, +} + +impl Heap { + pub fn class_id(&self, key: ClassKey) -> Option { + self.class_ids.get(&key).copied() + } + + pub fn abs_class(&self, a: AbsId) -> Option { + self[a].class + } +} + +/// Abstractions and classes are stored in dense vectors keyed by their own +/// id type, so `heap[abs]` and `heap[class]` are the natural spellings and +/// no caller has to write out the `as usize` cast that a raw index needs. +impl std::ops::Index for Heap { + type Output = Abstraction; + fn index(&self, a: AbsId) -> &Abstraction { + &self.abs[a.0 as usize] + } +} + +impl std::ops::IndexMut for Heap { + fn index_mut(&mut self, a: AbsId) -> &mut Abstraction { + &mut self.abs[a.0 as usize] + } +} + +impl std::ops::Index for Heap { + type Output = ClassInfo; + fn index(&self, c: ClassId) -> &ClassInfo { + &self.classes[c.0 as usize] + } +} + +impl std::ops::IndexMut for Heap { + fn index_mut(&mut self, c: ClassId) -> &mut ClassInfo { + &mut self.classes[c.0 as usize] + } +} + +/// Whether a region root counts as a class label. +/// +/// This is the one axis on which the two per-site receiver-class channels +/// differ. The agreement channel refuses it: a region is several classes, +/// so a site whose receiver is one has not settled on a class and must not +/// claim to have. The label channel accepts it, because the region rung's +/// whole job is to notice that every label a site saw lives in one region +/// -- and it cannot notice that if regions arrive unnamed. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum RegionLabels { + Accept, + Refuse, +} + +/// A converted snapshot value. +#[derive(Clone, Copy)] +enum SVal { + Fn(ScriptId), + Obj(SourceObjectId), + /// Prim mask plus the live value's interval claim (the heap-range + /// channel's snapshot seed: an Int32/integral-Double carries its + /// value, everything else the mask's default claim). + Prim(Prims, super::types::Interval), + /// A script-less (native) function with a name: carries the name's + /// string object id; `ts_of_sval` resolves it to a reserved native + /// fn id whose call result comes from the spec table. + NativeFn(SourceObjectId), +} + +fn sval(source: &Source, id: SourceObjectId) -> Option { + use super::types::Interval; + if id.is_other() { + return None; + } + Some(match source.object(id) { + SourceObject::Object(ObjectData { + kind: ObjectKind::Function, + script: Some(s), + .. + }) => SVal::Fn(ScriptId::new(s.id())), + SourceObject::Object(ObjectData { + kind: ObjectKind::Function, + script: None, + name: Some(n), + .. + }) if !n.is_other() => SVal::NativeFn(*n), + SourceObject::Object(ObjectData { + non_native: false, + kind: ObjectKind::Plain | ObjectKind::Array | ObjectKind::TypedArray(_), + .. + }) => SVal::Obj(id), + SourceObject::String(_) => SVal::Prim(PRIM_STRING, Interval::Empty), + SourceObject::Symbol => SVal::Prim(PRIM_SYMBOL, Interval::Empty), + SourceObject::Primitive(p) => match p { + Primitive::Undefined => SVal::Prim(PRIM_UNDEFINED, Interval::Empty), + Primitive::Null => SVal::Prim(PRIM_NULL, Interval::Empty), + Primitive::Boolean(_) => SVal::Prim(PRIM_BOOLEAN, Interval::Empty), + Primitive::Int32(v) => SVal::Prim(PRIM_INT32, Interval::of_value(i64::from(*v))), + Primitive::Double(v) => SVal::Prim(PRIM_DOUBLE, Interval::of_double(*v)), + }, + _ => return None, + }) +} + +impl Solver<'_> { + /// Build the snapshot-derived identity maps and pre-fill the shared + /// cells (gnames from the global object, aliased slots from captured + /// CallObjects). This is the whole snapshot integration: initial state. + pub(super) fn seed(&mut self) { + let mut script_proto: HashMap> = HashMap::default(); + let prototype = self.names_of.prototype; + for (id, obj) in self.source.objects() { + let SourceObject::Object(ObjectData { + non_native: false, + kind, + script, + properties, + .. + }) = obj + else { + continue; + }; + let Some(s) = script else { continue }; + if *kind == ObjectKind::Function { + self.heap + .script_fn_objs + .entry(ScriptId::new(s.id())) + .or_default() + .push(id); + } + for (k, v) in properties { + if k.is_other() { + continue; + } + let SourceObject::String(name) = self.source.object(*k) else { + continue; + }; + if !name + .chars() + .iter() + .copied() + .eq(self.names.get(prototype).iter().copied()) + { + continue; + } + let Some(SVal::Obj(p)) = sval(self.source, *v) else { + continue; + }; + let s = ScriptId::new(s.id()); + self.heap.proto_owner.entry(p).or_insert(s); + observe(&mut script_proto, s, p); + } + let _ = id; + } + // A constructor seen with two different `.prototype` objects has + // no class identity, so it keeps none. + self.heap.script_proto = script_proto + .into_iter() + .filter_map(|(s, p)| p.value().map(|p| (s, p))) + .collect(); + + // Gnames: the live global object's properties are the initial state + // of the GName cells. + if let Some(g) = self.source.global_object { + let props: Vec<(JsString, SourceObjectId)> = match self.source.object(g) { + SourceObject::Object(ObjectData { properties, .. }) => properties + .iter() + .filter_map(|(k, v)| { + if k.is_other() { + return None; + } + let SourceObject::String(name) = self.source.object(*k) else { + return None; + }; + Some((JsString::from_chars(name.chars().to_vec()), *v)) + }) + .collect(), + _ => Vec::new(), + }; + let mut seeded: HashSet = HashSet::default(); + let mut seeded_objs: HashSet = HashSet::default(); + for (name, v) in props { + let Some(v) = sval(self.source, v) else { + continue; + }; + let ts = self.ts_of_sval(v); + let name = self.names.intern(name.chars()); + seeded.insert(name); + if matches!(v, SVal::Obj(_)) { + seeded_objs.insert(name); + } + let cell = self.engine.cell(CellKey::GName(name)); + self.engine.raise(cell, &ts, (SEED, CTX0)); + } + // A binding transcribed as a bare native fn (`Object`) has no + // property cells of its own, so only a real transcribed OBJECT + // suppresses its namespace; the fn-only seed and the namespace + // join in the same gname cell (same reserved ctor id). + self.seed_native_namespaces(&seeded_objs); + self.seed_bare_natives(&seeded); + self.seed_intrinsic_natives(); + } + // Aliased slots: captured CallObject values. Load-bearing -- + // minified bundles reach most of their classes through short + // closure aliases rather than through named globals. + let mut slots: Vec<(SourceObjectId, EnvSlot, SourceObjectId)> = Vec::new(); + for (id, obj) in self.source.objects() { + let SourceObject::Scope(ScopeData { + env_slot_values, .. + }) = obj + else { + continue; + }; + for (slot, v) in env_slot_values { + slots.push((id, EnvSlot::new(*slot), *v)); + } + } + for (scope, slot, v) in slots { + let Some(v) = sval(self.source, v) else { + continue; + }; + let ts = self.ts_of_sval(v); + let cell = self.engine.cell(CellKey::Aliased { scope, slot }); + self.engine.raise(cell, &ts, (SEED, CTX0)); + } + // The global's abstraction must exist (bare-call receivers + // population-bind to it in the interval channel). Interned last: + // an earlier intern renumbers every downstream id for zero + // semantic difference. + if let Some(g) = self.source.global_object { + let _ = self.intern_snap(g); + } + } + + fn ts_of_sval(&mut self, v: SVal) -> TypeSet { + match v { + SVal::Fn(s) => TypeSet::fn_one(FnId::script(s)), + SVal::Obj(oid) => TypeSet::obj_one(self.intern_snap(oid)), + SVal::Prim(p, interval) => { + let mut ts = TypeSet::prim(p); + ts.interval = interval; + ts + } + SVal::NativeFn(nid) => { + let SourceObject::String(s) = self.source.object(nid) else { + return TypeSet::default(); + }; + let chars = s.chars().to_vec(); + // The Array/TA constructor names keep their existing + // reserved ids (allocation semantics; the scan-side gname + // path mints the same ids, and the sets must agree). + if builtins::is_array_ctor_name(&chars) { + return TypeSet::fn_one(FnId::ARRAY_CTOR); + } + if let Some(ta) = builtins::ta_kind_for_ctor_name(&chars) { + return TypeSet::fn_one(FnId::typed_array_ctor(ta)); + } + let name = self.names.intern(&chars); + TypeSet::fn_one(self.native_id(NativeKind::Bare, name)) + } + } + } + + /// Modeled self-hosted intrinsics: the scan reads `GetIntrinsic name` + /// as the %-mangled gname (the intrinsic environment is not the global + /// object, and '%' cannot appear in a user identifier), so seeding the + /// mangled cell with the named native resolves the kernel calls the + /// self-hosted string code bottoms out in. Leaving a kernel such as + /// `Substring` unresolved would make the RegExp-replace buildup's + /// results unresolved evidence and push every concat of one off the + /// Opt track. + fn seed_intrinsic_natives(&mut self) { + // (intrinsic name, bare name the result mask resolves through). + const INTRINSICS: &[(&str, &str)] = &[ + ("Substring", "Substring"), + ("ToString", "ToString"), + ("ToObject", "ToObject"), + ("IsObject", "IsObject"), + ("ToLength", "ToLength"), + ("Number_isNaN", "Number_isNaN"), + ( + "UnsafeGetStringFromReservedSlot", + "UnsafeGetStringFromReservedSlot", + ), + ("RegExpMatcher", "RegExpMatcher"), + ("RegExpSearcher", "RegExpSearcher"), + ("RegExpSearcherLastLimit", "RegExpSearcherLastLimit"), + ("RegExpHasCaptureGroups", "RegExpHasCaptureGroups"), + ("RegExpGetSubstitution", "RegExpGetSubstitution"), + ("IsOptimizableRegExpObject", "IsOptimizableRegExpObject"), + ("SubstringKernel", "SubstringKernel"), + ("StringSplitString", "StringSplitString"), + ("GuardToSetObject", "GuardToSetObject"), + ("ToInteger", "ToInteger"), + ( + "UnsafeGetInt32FromReservedSlot", + "UnsafeGetInt32FromReservedSlot", + ), + ("ThrowIncompatibleMethod", "ThrowIncompatibleMethod"), + ("ThrowTypeError", "ThrowTypeError"), + ("IsCallable", "IsCallable"), + ("AdvanceStringIndex", "AdvanceStringIndex"), + ("GuardToMapObject", "GuardToMapObject"), + ("std_Math_max", "max"), + ("std_Math_min", "min"), + ]; + for &(n, result_as) in INTRINSICS { + let bare: Vec = result_as.encode_utf16().collect(); + let mut chars: Vec = vec![u16::from(b'%')]; + chars.extend(n.encode_utf16()); + let mangled = self.names.intern(&chars); + let id = self.natives.intern(NativeKind::Bare, mangled, &bare); + let cell = self.engine.cell(CellKey::GName(mangled)); + self.engine.raise(cell, &TypeSet::fn_one(id), (SEED, CTX0)); + } + } + + /// Bare global native converters the walker leaves other (a native + /// JSFunction is untranscribable): seed their GName cells with the + /// modeled native id, same likely-not-proof contract as the + /// namespaces below -- every consumer guards callee identity at + /// runtime, so a shadowed binding self-misses. + fn seed_bare_natives(&mut self, seeded: &HashSet) { + for n in [ + "parseInt", + "parseFloat", + "isNaN", + "isFinite", + "print", + "Error", + "TypeError", + "RangeError", + "ReferenceError", + "SyntaxError", + "EvalError", + "URIError", + ] { + let name = self.names.intern_str(n); + if seeded.contains(&name) { + continue; + } + let id = self.native_id(NativeKind::Bare, name); + let cell = self.engine.cell(CellKey::GName(name)); + self.engine.raise(cell, &TypeSet::fn_one(id), (SEED, CTX0)); + } + } + + /// Synthesize the builtin namespaces the walker could not transcribe: + /// a namespace whose global binding was captured as a real object is + /// skipped (its own property cells win); an absent-or-other binding + /// gets a synthetic abstraction with spec-seeded method/const cells. + fn seed_native_namespaces(&mut self, seeded: &HashSet) { + for (i, ns) in builtins::NAMESPACES.iter().enumerate() { + let gname = self.names.intern_str(ns.global); + if seeded.contains(&gname) { + continue; + } + let abs = self.new_abs(AbsKey::NativeNs(u8::try_from(i).unwrap())); + self.heap[abs].seeded = true; + for m in ns.methods { + let mname = self.names.intern_str(m); + let id = self.native_id(NativeKind::Bare, mname); + let cell = self.field_cell(abs, mname); + self.engine.raise(cell, &TypeSet::fn_one(id), (SEED, CTX0)); + } + for (c, mask) in ns.consts { + let cname = self.names.intern_str(c); + let cell = self.field_cell(abs, cname); + self.engine.raise(cell, &TypeSet::prim(*mask), (SEED, CTX0)); + } + let mut ts = TypeSet::obj_one(abs); + if ns.ctor { + let id = self.native_id(NativeKind::Bare, gname); + ts.fns = super::types::BoundedFnSet::one(id); + } + let cell = self.engine.cell(CellKey::GName(gname)); + self.engine.raise(cell, &ts, (SEED, CTX0)); + } + } + + /// Get-or-mint the reserved fn id for a (kind, name) native, resolving + /// its spec result mask once. + fn native_id(&mut self, kind: NativeKind, name: NameId) -> FnId { + let chars = self.names.get(name).to_vec(); + self.natives.intern(kind, name, &chars) + } + + /// The modeled call result for a named-native id: the spec table's + /// mask, or the unknown evidence bit for natives we do not model. + /// `args_integral`: every argument at the site is integrally ranged + /// (the integral-preserving natives claim I53 under it). + pub(super) fn native_ret(&self, f: FnId, args_integral: bool) -> TypeSet { + let info = self.natives.get(f); + let mask = info.and_then(|i| i.result); + let mut ts = mask.map_or_else(TypeSet::unknown_evidence, TypeSet::prim); + if mask.is_some() + && info.is_some_and(|i| { + let name = self.names.get(i.name); + builtins::integral_native(name) + || (args_integral && builtins::integral_preserving_native(name)) + }) + { + ts.range = super::types::Range::I53; + } + ts + } + + fn new_abs(&mut self, key: AbsKey) -> AbsId { + let id = AbsId(u32::try_from(self.heap.abs.len()).unwrap()); + self.heap.abs.push(Abstraction { + key, + class: None, + proto: ProtoLink::None, + owner_class: None, + proto_of: None, + ta_kind: None, + is_array: false, + seeded: false, + }); + // The engine's parallel join-metadata vec (consulted by every + // join) must grow in lockstep. + self.engine.abs_labels.push(AbsLabels::default()); + self.heap.abs_ids.insert(key, id); + id + } + + fn set_abs_class(&mut self, a: AbsId, c: ClassId) { + self.heap[a].class = Some(c); + self.engine.abs_labels[a.0 as usize].class = Some(c); + } + + pub(super) fn intern_snap(&mut self, oid: SourceObjectId) -> AbsId { + if let Some(&a) = self.heap.abs_ids.get(&AbsKey::Snap(oid)) { + return a; + } + // Insert before resolving class/proto: the bridge and proto walks + // may re-enter for objects up the chain. + let a = self.new_abs(AbsKey::Snap(oid)); + self.engine.abs_labels[a.0 as usize].snap = true; + let sobj = self.source.object(oid); + let (kind, proto) = match sobj { + SourceObject::Object(ObjectData { + non_native: false, + kind, + proto, + .. + }) => (*kind, *proto), + _ => (ObjectKind::Other, None), + }; + self.heap[a].is_array = kind == ObjectKind::Array; + self.engine.abs_labels[a.0 as usize].array = kind == ObjectKind::Array; + if let ObjectKind::TypedArray(code) = kind { + if let Some(&tk) = crate::opsem::TaKind::ALL.get(usize::from(code).wrapping_sub(1)) { + self.heap[a].ta_kind = Some(tk); + } + } + if let Some(pid) = proto { + // The concrete->class bridge: an object whose `[[Prototype]]` is + // some constructor's `.prototype` is an instance of that class. + if let Some(&ctor) = self.heap.proto_owner.get(&pid) { + let c = self.class_for_fn(ctor); + self.set_abs_class(a, c); + } + let pa = self.intern_snap(pid); + self.heap[a].proto = ProtoLink::Abs(pa); + } + a + } + + pub(super) fn intern_alloc( + &mut self, + script: ScriptId, + pc: Pc, + ctx: CtxId, + class: Option, + is_array: bool, + ta_kind: Option, + ) -> AbsId { + let key = AbsKey::Alloc { script, pc, ctx }; + if let Some(&a) = self.heap.abs_ids.get(&key) { + return a; + } + let a = self.new_abs(key); + self.heap[a].is_array = is_array; + self.engine.abs_labels[a.0 as usize].array = is_array; + self.heap[a].ta_kind = ta_kind; + match class { + Some(c) => { + self.set_abs_class(a, c); + self.heap[a].proto = ProtoLink::Abs(self.heap[c].proto_abs); + } + None => { + let c = self.site_class(script, pc); + self.set_abs_class(a, c); + if ta_kind.is_some() { + self.heap[c].ta_kind = ta_kind; + } + if is_array { + self.heap[c].is_array = true; + self.engine.array_classes.insert(c); + } + } + } + a + } + + /// The per-site pseudo-class of a classless allocation site. + fn site_class(&mut self, script: ScriptId, pc: Pc) -> ClassId { + let key = ClassKey::Site(Site::new(script, pc)); + match self.heap.class_ids.get(&key) { + Some(&c) => c, + None => self.new_class(key, None), + } + } + + fn intern_fn_obj(&mut self, script: ScriptId) -> AbsId { + if let Some(&a) = self.heap.abs_ids.get(&AbsKey::FnObj(script)) { + return a; + } + self.new_abs(AbsKey::FnObj(script)) + } + + /// Mint (or fetch) the class of constructor script `f`. Identity: + /// the snapshot-agreed `.prototype` object, else the script. + /// Mint a class and its synthetic prototype abstraction. + /// + /// The prototype abstraction back-references the class, so the class + /// row is reserved first and its `proto_abs` patched afterwards; that + /// ordering is why the row is briefly published with the `AbsId::MAX` + /// sentinel, and why this is one function rather than something each + /// caller assembles. + fn new_class(&mut self, key: ClassKey, ctor: Option) -> ClassId { + let c = ClassId(u32::try_from(self.heap.classes.len()).unwrap()); + self.heap.classes.push(ClassInfo { + key, + ctor, + proto_abs: AbsId(u32::MAX), + sources: Vec::new(), + ta_kind: None, + is_array: false, + }); + self.heap.class_ids.insert(key, c); + let pa = self.new_abs(AbsKey::ProtoOf(c)); + self.heap[pa].proto_of = Some(c); + self.heap[pa].owner_class = Some(c); + self.heap[c].proto_abs = pa; + c + } + + pub(super) fn class_for_fn(&mut self, f: ScriptId) -> ClassId { + let key = match self.heap.script_proto.get(&f) { + Some(&p) => ClassKey::Proto(p), + None => ClassKey::Script(f), + }; + self.class_for_key(key, Some(f), f) + } + + /// Get-or-mint the class named by `key`, homing `f`'s `this` to it and + /// registering the concrete prototype object as a method-table source + /// when the key names one. + fn class_for_key(&mut self, key: ClassKey, ctor: Option, f: ScriptId) -> ClassId { + if let Some(&c) = self.heap.class_ids.get(&key) { + return c; + } + let c = self.new_class(key, ctor); + if let ClassKey::Proto(p) = key { + let src = self.intern_snap(p); + self.register_proto_source(c, src); + } + self.this_home_add(f, c); + c + } + + /// Mint (or fetch) the class of a shared-generated ctor's concrete + /// class object: identity is the object's own `.prototype` (many + /// classes share the script, so `class_for_fn`'s script fallback + /// collapses them). Same construction as `class_for_fn`'s Proto arm; + /// registering the concrete prototype source homes its method + /// scripts (`note_method_home`), which pins the methods' `this` to + /// the class. + pub(super) fn class_for_ctor_proto(&mut self, f: ScriptId, p: SourceObjectId) -> ClassId { + // No ctor attribution: the script is shared, and a ctor-keyed + // group id would collapse every such class into one group. A + // class-keyed group (the lit-class rule) keeps each its own + // layout-key range for the per-site emission. + self.class_for_key(ClassKey::Proto(p), None, f) + } + + /// Pre-solve resolution of shared-generated-ctor construct sites + /// (the prototype.js `Class.create()` idiom). Purely syntactic + + /// concrete: a ctor script whose `this` events are delegations only + /// (no direct writes) is shared-generated; each construct site's + /// callee def chain (gname/aliased roots, property hops) resolves + /// against the snapshot to the concrete function object, whose + /// `.prototype` keys the per-class identity and carries the member + /// the `this..apply` dispatch reaches. Fills + /// `site_ctor_class` (consumed at construct evaluation) and + /// `shared_ctor_sites` (consumed by the emit-phase layout minting). + pub(super) fn resolve_shared_ctor_sites(&mut self) { + use super::engine::CKey; + use super::scan::TEvent; + let mut deleg_pcs: HashMap> = HashMap::default(); + for (&sid, evs) in &self.tables.this_events { + if evs.iter().any(|e| matches!(e, TEvent::Write(_))) { + continue; + } + let pcs: Vec = evs + .iter() + .filter_map(|e| match e { + TEvent::Deleg(pc) => Some(*pc), + _ => None, + }) + .collect(); + if !pcs.is_empty() { + deleg_pcs.insert(sid, pcs); + } + } + if deleg_pcs.is_empty() { + return; + } + let mut read_defs: HashMap<(ScriptId, VarId), (CKey, NameId)> = HashMap::default(); + let mut csites: Vec<(Site, CKey)> = Vec::new(); + let mut apply_tgt: HashMap = HashMap::default(); + for ci in 0..self.engine.cons.len() { + let script = self.engine.con_script[ci]; + match &self.engine.cons[ci] { + Constraint::Read { + recv, + name, + dst: CKey::Var(v), + .. + } => { + read_defs.insert((script, *v), (*recv, *name)); + } + Constraint::Call { + callee, + pc, + construct: true, + .. + } => { + csites.push((Site::new(script, *pc), *callee)); + } + Constraint::Apply { target, pc, .. } => { + apply_tgt.insert(Site::new(script, *pc), *target); + } + _ => {} + } + } + let mut shared_init: HashMap = HashMap::default(); + for (&f, pcs) in &deleg_pcs { + for &dpc in pcs { + let Some(&CKey::Var(tv)) = apply_tgt.get(&Site::new(f, dpc)) else { + continue; + }; + let Some(&(CKey::This, n)) = read_defs.get(&(f, tv)) else { + continue; + }; + shared_init.insert(f, n); + break; + } + } + if shared_init.is_empty() { + return; + } + fn obj_prop( + source: &Source, + names: &super::types::Names, + oid: SourceObjectId, + name: NameId, + ) -> Option { + if oid.is_other() { + return None; + } + let SourceObject::Object(ObjectData { properties, .. }) = source.object(oid) else { + return None; + }; + let want = names.get(name); + for (k, v) in properties { + if k.is_other() { + continue; + } + let SourceObject::String(s) = source.object(*k) else { + continue; + }; + if s.chars() == want.chars() { + return Some(*v); + } + } + None + } + fn resolve_concrete( + source: &Source, + names: &super::types::Names, + read_defs: &HashMap<(ScriptId, VarId), (CKey, NameId)>, + script: ScriptId, + key: CKey, + depth: u32, + ) -> Option { + if depth > 8 { + return None; + } + match key { + CKey::GName(n) => { + let g = source.global_object?; + obj_prop(source, names, g, n) + } + CKey::Var(v) => { + let &(recv, name) = read_defs.get(&(script, v))?; + let r = resolve_concrete(source, names, read_defs, script, recv, depth + 1)?; + obj_prop(source, names, r, name) + } + CKey::Aliased { scope, slot } => { + let SourceObject::Scope(ScopeData { + env_slot_values, .. + }) = source.object(scope) + else { + return None; + }; + env_slot_values + .iter() + .find(|(s, _)| EnvSlot::new(*s) == slot) + .map(|(_, v)| *v) + } + _ => None, + } + } + let n_prototype = self.names_of.prototype; + // Two passes: resolve every site first, then commit only ctors + // that are truly shared (>= 2 distinct prototypes across their + // sites). A single-class apply wrapper is fully served by the + // script-keyed machinery, and swapping its model identity for a + // proto-keyed class costs it real speed for nothing. + let mut resolved: Vec<(Site, SharedCtorSite)> = Vec::new(); + let mut protos_of: HashMap> = HashMap::default(); + for (site, callee) in csites { + let Some(fo) = + resolve_concrete(self.source, &self.names, &read_defs, site.script, callee, 0) + else { + continue; + }; + let Some(f) = self.source.fn_script(fo) else { + continue; + }; + let Some(&init_name) = shared_init.get(&f) else { + continue; + }; + let Some(proto) = obj_prop(self.source, &self.names, fo, n_prototype) else { + continue; + }; + if proto.is_other() { + continue; + } + let Some(init_fo) = obj_prop(self.source, &self.names, proto, init_name) else { + continue; + }; + let Some(init_sid) = self.source.fn_script(init_fo) else { + continue; + }; + protos_of.entry(f).or_default().insert(proto); + resolved.push(( + site, + SharedCtorSite { + ctor: f, + proto, + init: init_sid, + }, + )); + } + for (site, shared) in resolved { + if protos_of.get(&shared.ctor).is_none_or(|ps| ps.len() < 2) { + continue; + } + let c = self.class_for_ctor_proto(shared.ctor, shared.proto); + self.site_ctor_class.insert(site, c); + self.shared_ctor_sites.insert(site, shared); + } + } + + /// Register a concrete prototype object as a source of `c`'s method + /// table: per-name standing links both for names already interned and + /// (via `field_cell`'s owner check) names interned later. Never value + /// flow: the object's cells feed the table, nothing is merged. + pub(super) fn register_proto_source(&mut self, c: ClassId, src: AbsId) { + self.register_proto_source_impl(c, src, true); + } + + /// The linking half of `register_proto_source` alone: the object's + /// cells feed `c`'s method table, but `c` neither owns the object nor + /// homes its methods. For the fn-keyed class of a SHARED ctor script + /// (prototype.js `Class.create()`): `SharedCtor.prototype.m(...)` + /// resolution needs the union table over every sharing class's + /// prototype, while homing to that one class would demote every + /// per-prototype pin. + pub(super) fn link_proto_table(&mut self, c: ClassId, src: AbsId) { + self.register_proto_source_impl(c, src, false); + } + + fn register_proto_source_impl(&mut self, c: ClassId, src: AbsId, own: bool) { + if self.heap[c].sources.contains(&src) { + return; + } + self.heap[c].sources.push(src); + if let AbsKey::Alloc { script, pc, .. } = self.heap[src].key { + self.heap.site_is_proto.insert(Site::new(script, pc)); + } + if own && self.heap[src].owner_class.is_none() { + self.heap[src].owner_class = Some(c); + } + self.ensure_seeded(src); + let pa = self.heap[c].proto_abs; + // Class-level upward link: the table's chain continues where the + // concrete prototype's chain does (first install wins). + if self.heap[pa].proto == ProtoLink::None { + if let ProtoLink::Abs(up) = self.heap[src].proto { + self.heap[pa].proto = ProtoLink::Abs(up); + let s = self.engine.cell(CellKey::ProtoSentinel(pa)); + self.engine + .raise(s, &TypeSet::prim(PRIM_NULL), (SEED, CTX0)); + } + } + let names: Vec = self + .heap + .fields_of + .get(&pa) + .cloned() + .unwrap_or_default() + .into_iter() + .chain(self.heap.fields_of.get(&src).cloned().unwrap_or_default()) + .collect(); + for name in names { + let sc = self.field_cell(src, name); + let dc = self.field_cell(pa, name); + self.engine.link(sc, dc); + } + // Methods written into the object before it became a prototype + // (`F.prototype = {m: fn}` inits the literal first) still get homed. + if own { + for name in self.heap.fields_of.get(&src).cloned().unwrap_or_default() { + let cell = self.field_cell(src, name); + let v = self.engine.ts(cell).clone(); + self.note_method_home(c, &v); + } + } + } + + /// Seed `abs` from the snapshot if it has not been, then intern its + /// field cell. + /// + /// The order matters and is why this is a helper rather than + /// `field_cell` doing the seeding itself: seeding interns cells of its + /// own, so folding it in would mint this field's cell before them and + /// renumber the cell space. + fn seeded_field_cell(&mut self, abs: AbsId, name: NameId) -> CellId { + self.ensure_seeded(abs); + self.field_cell(abs, name) + } + + /// The bundle-wide union of every array abstraction's elements. + fn elems_union(&mut self) -> CellId { + self.engine.cell(CellKey::ArrayElemsUnion) + } + + /// Read a cell on behalf of `user` and join it into `out`. + fn read_join(&mut self, cell: CellId, user: (ConId, CtxId), out: &mut TypeSet) { + let v = self.engine.read(cell, user); + let _ = self.engine.join_ts(out, &v); + } + + /// The class of the view a receiver of class `c` reads `name` through. + /// Elements are read through the region root, so a merged array + /// population shares one element node; every other name reads its own + /// class's view. + fn view_class(&self, c: ClassId, is_elems: bool) -> ClassId { + if is_elems { + self.engine.region_root(c) + } else { + c + } + } + + /// Intern `Field(abs, name)`, installing its standing edges on first + /// creation: proto-source feeds, method-table feeds, and the ClassView + /// feed for classed instances. + pub(super) fn field_cell(&mut self, abs: AbsId, name: NameId) -> CellId { + let key = CellKey::Field { abs, name }; + if let Some(c) = self.engine.lookup(key) { + return c; + } + let cell = self.engine.cell(key); + self.heap.fields_of.entry(abs).or_default().push(name); + let info = &self.heap[abs]; + let proto_of = info.proto_of; + let owner = info.owner_class; + let class = info.class; + let info_is_array = info.is_array; + if let Some(c) = proto_of { + for src in self.heap[c].sources.clone() { + let sc = self.field_cell(src, name); + self.engine.link(sc, cell); + } + } + if let Some(c) = owner { + if proto_of != Some(c) { + let pa = self.heap[c].proto_abs; + let dc = self.field_cell(pa, name); + self.engine.link(cell, dc); + } + } + if let Some(c) = class { + let view = self.engine.cell(CellKey::ClassView { class: c, name }); + self.engine.link(cell, view); + } + if name == self.names_of.elems && info_is_array { + let union = self.engine.cell(CellKey::ArrayElemsUnion); + self.engine.link(cell, union); + } + cell + } + + fn class_field_cell(&mut self, class: ClassId, name: NameId) -> CellId { + let key = CellKey::ClassField { class, name }; + if let Some(c) = self.engine.lookup(key) { + return c; + } + let cell = self.engine.cell(key); + let view = self.engine.cell(CellKey::ClassView { class, name }); + self.engine.link(cell, view); + cell + } + + /// Pre-fill a snapshot abstraction's field cells from the transcribed + /// heap, once, on first field access. + pub(super) fn ensure_seeded(&mut self, abs: AbsId) { + if self.heap[abs].seeded { + return; + } + self.heap[abs].seeded = true; + let oid = match self.heap[abs].key { + AbsKey::Snap(oid) => oid, + AbsKey::FnObj(s) => { + self.seed_fn_obj(abs, s); + return; + } + _ => return, + }; + let (props, elems): (Vec<(JsString, SourceObjectId)>, Vec) = + match self.source.object(oid) { + SourceObject::Object(ObjectData { + non_native: false, + properties, + elements, + .. + }) => ( + properties + .iter() + .filter_map(|(k, v)| { + if k.is_other() { + return None; + } + let SourceObject::String(name) = self.source.object(*k) else { + return None; + }; + Some((JsString::from_chars(name.chars().to_vec()), *v)) + }) + .collect(), + elements.iter().map(|(_, v)| *v).collect(), + ), + _ => return, + }; + for (name, v) in props { + let Some(v) = sval(self.source, v) else { + continue; + }; + let ts = self.ts_of_sval(v); + let name = self.names.intern(name.chars()); + let cell = self.field_cell(abs, name); + self.engine.raise(cell, &ts, (SEED, CTX0)); + } + if !elems.is_empty() { + let mut ts = TypeSet::default(); + let mut fns: Vec = Vec::new(); + for v in elems { + let Some(v) = sval(self.source, v) else { + continue; + }; + if let SVal::Fn(s) = v { + fns.push(FnId::script(s)); + } + let t = self.ts_of_sval(v); + ts.join_from(&t, &self.engine.abs_labels, &mut self.engine.sink); + } + // Fn-table member list: the elems cell saturates + // to fn-multi past the BoundedFnSet cap, but the live wizer image + // holds the load-time population -- seed it for arg-binding + // (runtime registrations extend it via the write-side capture). + if !fns.is_empty() { + self.add_table_members(abs, &fns); + } + if !ts.is_empty() { + let name = self.names_of.elems; + let cell = self.field_cell(abs, name); + self.engine.raise(cell, &ts, (SEED, CTX0)); + } + } + } + + /// Seed a script's `FnObj` statics space from every transcribed + /// snapshot closure of the script (statics installed at wizen time + /// exist only there; without this a `F.staticName` read is Empty + /// forever). `.prototype` is identity, handled by the class maps. + fn seed_fn_obj(&mut self, abs: AbsId, script: ScriptId) { + let prototype = self.names_of.prototype; + let oids = self + .heap + .script_fn_objs + .get(&script) + .cloned() + .unwrap_or_default(); + for oid in oids { + let props: Vec<(JsString, SourceObjectId)> = match self.source.object(oid) { + SourceObject::Object(ObjectData { + non_native: false, + properties, + .. + }) => properties + .iter() + .filter_map(|(k, v)| { + if k.is_other() { + return None; + } + let SourceObject::String(name) = self.source.object(*k) else { + return None; + }; + Some((JsString::from_chars(name.chars().to_vec()), *v)) + }) + .collect(), + _ => continue, + }; + for (name, v) in props { + let Some(v) = sval(self.source, v) else { + continue; + }; + // Function- and prim-valued statics only: instance-valued + // statics (BigInteger.ZERO) would join snapshot instances + // into alloc-site-pure populations, merging classes that + // never otherwise meet. + if matches!(v, SVal::Obj(_)) { + continue; + } + let name = self.names.intern(name.chars()); + if name == prototype { + continue; + } + let ts = self.ts_of_sval(v); + let cell = self.field_cell(abs, name); + self.engine.raise(cell, &ts, (SEED, CTX0)); + } + } + } + + /// Fn-table member capture at an elems write: a + /// single-fn write records directly; a saturated write sourced from + /// an arg row merges that row's per-site fn record and registers the + /// reverse feed so later registrations append without a re-fire. + fn note_table_members( + &mut self, + a: AbsId, + src: super::engine::CKey, + script: ScriptId, + v: &TypeSet, + ) { + if v.fns.is_multi() { + let super::engine::CKey::Arg(i) = src else { + return; + }; + let feeds = self.arg_row_tables.entry((script, i)).or_default(); + if !feeds.contains(&a) { + feeds.push(a); + } + let ids: Vec = self + .arg_fn_members + .get(&(script, i)) + .map_or_else(Vec::new, |s| s.iter().copied().collect()); + if !ids.is_empty() { + self.add_table_members(a, &ids); + } + return; + } + let ids: Vec = v + .fns + .ids() + .iter() + .copied() + .filter(|&f| !f.is_builtin()) + .collect(); + if !ids.is_empty() { + self.add_table_members(a, &ids); + } + } + + /// Monotone chain join from `holder`'s proto upward: joins each level's + /// own field cell, subscribing the reader along the way; a dead end + /// subscribes the holder's proto sentinel so a later install re-fires. + fn chain_join(&mut self, holder: AbsId, name: NameId, user: (ConId, CtxId), out: &mut TypeSet) { + let mut cur = holder; + for _ in 0..CHAIN_DEPTH { + match self.heap[cur].proto { + ProtoLink::None => { + let s = self.engine.cell(CellKey::ProtoSentinel(cur)); + let _ = self.engine.read(s, user); + return; + } + ProtoLink::Abs(p) => { + let f = self.seeded_field_cell(p, name); + self.read_join(f, user, out); + cur = p; + } + } + } + // Ran out of depth with the chain still going: the levels above + // were never joined. + self.stats.caps.proto_chain += 1; + } + + /// Add `c` to `sid`'s home classes (cell-side this-attribution): + /// installs ThisField -> ClassField links for every name already + /// this-written, and propagates through recorded this-forwarding + /// delegation edges. Capped: a script homed everywhere is a shared + /// helper, and linking it into every class merges their field cells + /// into one useless claim. + pub(super) fn this_home_add(&mut self, sid: ScriptId, c: ClassId) { + { + let homes = self.this_homes.entry(sid).or_default(); + if homes.contains(&c) { + return; + } + if homes.len() >= MAX_HOMES { + self.stats.caps.this_homes += 1; + return; + } + homes.push(c); + } + for name in self.this_field_names.get(&sid).cloned().unwrap_or_default() { + let src = self.engine.cell(CellKey::ThisField { script: sid, name }); + let dst = self.class_field_cell(c, name); + self.engine.link(src, dst); + } + for d in self.this_delegs.get(&sid).cloned().unwrap_or_default() { + self.this_home_add(d, c); + } + } + + /// Record a this-forwarding call edge (caller `f` hands its `this` to + /// callee `g`): `g`'s this-writes attribute to `f`'s home classes. + pub(super) fn this_deleg_add(&mut self, f: ScriptId, g: ScriptId) { + let ds = self.this_delegs.entry(f).or_default(); + if ds.contains(&g) { + return; + } + ds.push(g); + for c in self.this_homes.get(&f).cloned().unwrap_or_default() { + self.this_home_add(g, c); + } + } + + /// Raise a this-write into the script's ThisField cell (minting its + /// home links on first use of the name). + fn this_field_raise(&mut self, sid: ScriptId, name: NameId, v: &TypeSet, user: (ConId, CtxId)) { + let key = CellKey::ThisField { script: sid, name }; + let cell = if let Some(c) = self.engine.lookup(key) { + c + } else { + let c = self.engine.cell(key); + self.this_field_names.entry(sid).or_default().push(name); + for home in self.this_homes.get(&sid).cloned().unwrap_or_default() { + let dst = self.class_field_cell(home, name); + self.engine.link(c, dst); + } + c + }; + self.engine.raise(cell, v, user); + } + + /// Method-home attribution: a function value written into a class's + /// method table homes the method script to the ctor (first install + /// wins; a differing second install demotes). + fn note_method_home(&mut self, owner: ClassId, v: &TypeSet) { + if v.fns.is_multi() { + return; + } + // The this-assertion: a method homed to a likely-class asserts that + // `this` entering the method IS that class, independent of call + // resolution -- polymorphic dispatch sites (a task queue holding + // four task kinds) leave the receiver AnyObject, but each method + // body still reads/writes its own class's cells precisely. A method + // installed on two classes gets both seeds and joins to AnyObject: + // the honest answer for genuinely shared methods. + for m in v.fns.scripted() { + // Pin bookkeeping: a single-homed method's `this` is asserted; + // `bind_this_ok` refuses worse-than-asserted (AnyObject/AnyOf) + // receivers. A second differing install unpins (shared method: + // both seeds join to AnyObject below, callers bind normally). + let first_pin = { + let pin = self.this_pin.entry(m).or_default(); + let fresh = *pin == Agreed::Unset; + pin.observe(owner); + fresh + }; + if first_pin { + self.this_home_add(m, owner); + } + let this_cell = self.engine.cell(CellKey::This { + script: m, + ctx: CTX0, + }); + let ts = TypeSet { + obj: ObjType::ClassAny(owner), + ..TypeSet::default() + }; + self.engine.raise(this_cell, &ts, (SEED, CTX0)); + } + let Some(ctor) = self.heap[owner].ctor else { + return; + }; + for m in v.fns.scripted() { + observe(&mut self.heap.method_home, m, ctor); + } + } + + pub(super) fn eval_heap(&mut self, con: ConId, ctx: CtxId) -> bool { + let sid = self.engine.con_script[con.0 as usize]; + let user = (con, ctx); + match self.engine.cons[con.0 as usize].clone() { + Constraint::Read { + recv, + name, + dst, + pc, + callee_pos, + } => { + let r = self.engine.resolve(sid, ctx, recv); + let rts = self.engine.read(r, user); + self.trace_site_eval(sid, pc, ctx, recv, r, &rts); + let d = self.engine.resolve(sid, ctx, dst); + let mut out = TypeSet::default(); + let region_contributed = self.read_into(&rts, name, callee_pos, user, &mut out); + if callee_pos && region_contributed { + if let super::engine::CKey::Var(v) = dst { + self.region_calls.insert((sid, v)); + } + } + // Recorded for every elems read, not just callee position: + // an apply-form dispatch (`action[0].call(...)`) consumes + // the read's result as its TARGET, and the fn-table + // fallback needs the same provenance there. + if name == self.names_of.elems { + if let super::engine::CKey::Var(v) = dst { + self.elems_callee_vars.insert((sid, v), recv); + } + } + self.note_site_recv(sid, pc, &rts); + self.note_site_evidence(sid, pc, name, &rts, Some(&out)); + self.engine.raise(d, &out, user); + true + } + Constraint::Write { + recv, + name, + src, + pc, + } => { + let r = self.engine.resolve(sid, ctx, recv); + let rts = self.engine.read(r, user); + let s = self.engine.resolve(sid, ctx, src); + let v = self.engine.read(s, user); + if name == self.names_of.elems { + if let ObjType::One(a) = rts.obj { + self.note_table_members(a, src, sid, &v); + if let AbsKey::Alloc { script, pc, .. } = self.heap[a].key { + self.heap.dyn_named_writes.insert(Site::new(script, pc)); + } + } + } + self.note_site_evidence(sid, pc, name, &rts, None); + let this_recv = recv == super::engine::CKey::This && name != self.names_of.elems; + if this_recv { + self.this_field_raise(sid, name, &v, user); + } + self.write_into(&rts, name, &v, this_recv, user); + true + } + Constraint::Alloc { dst, pc, kind } => { + let abs = match kind { + AllocKind::Snapshot(oid) => self.intern_snap(oid), + AllocKind::Plain => self.intern_alloc(sid, pc, ctx, None, false, None), + AllocKind::Array => self.intern_alloc(sid, pc, ctx, None, true, None), + AllocKind::TypedArray(k) => { + self.intern_alloc(sid, pc, ctx, None, false, Some(k)) + } + }; + let d = self.engine.resolve(sid, ctx, dst); + self.engine.raise(d, &TypeSet::obj_one(abs), user); + true + } + Constraint::ElemBuiltin { + recv, + arg, + ret, + pc: _, + kind, + } => { + let r = self.engine.resolve(sid, ctx, recv); + let rts = self.engine.read(r, user); + let d = self.engine.resolve(sid, ctx, ret); + let elems = self.names_of.elems; + if kind == ElemBuiltinKind::Write { + if let Some(arg) = arg { + let s = self.engine.resolve(sid, ctx, arg); + let v = self.engine.read(s, user); + self.write_into(&rts, elems, &v, false, user); + } + self.engine.raise(d, &TypeSet::prim(PRIM_INT32), user); + } else { + let mut out = TypeSet::prim(PRIM_UNDEFINED); + let _ = self.read_into(&rts, elems, false, user, &mut out); + self.engine.raise(d, &out, user); + } + true + } + _ => false, + } + } + + /// Read `name` off every part of receiver typeset `rts`, joining what + /// each part yields into `out`. Returns whether a region's method + /// table contributed a callee -- the caller records such sites so the + /// emission can tell a flow-scoped dispatch set from a resolved one. + fn read_into( + &mut self, + rts: &TypeSet, + name: NameId, + callee_pos: bool, + user: (ConId, CtxId), + out: &mut TypeSet, + ) -> bool { + self.trace_field("read", name, rts, None, user); + let mut region_contributed = false; + let is_elems = name == self.names_of.elems; + let chain_ok = !is_elems; + // Prim-receiver method resolution: a call off a known-string or + // known-numeric receiver resolves modeled String/Number.prototype + // natives (the receiver kind disambiguates names like slice). + if callee_pos && rts.prims.intersects(PRIM_STRING | PRIM_INT32 | PRIM_DOUBLE) { + let chars = self.names.get(name).to_vec(); + if rts.prims.intersects(PRIM_STRING) + && builtins::prim_method(NativeKind::StringMethod, &chars) + { + let id = self.native_id(NativeKind::StringMethod, name); + out.fns.insert(id, &mut self.engine.sink.dropped_fns); + } + if rts.prims.intersects(PRIM_INT32 | PRIM_DOUBLE) + && builtins::prim_method(NativeKind::NumberMethod, &chars) + { + let id = self.native_id(NativeKind::NumberMethod, name); + out.fns.insert(id, &mut self.engine.sink.dropped_fns); + } + } + if rts.fns.is_multi() { + // A lost-identity fn receiver: the property value is unknown, + // but contributing fn-multi would poison the callee sets of + // precise sibling contexts -- unknown contributes nothing. + let mut any = TypeSet::unresolved(); + any.fns = Default::default(); + let _ = self.engine.join_ts(out, &any); + } else { + for f in rts.fns.scripted() { + if name == self.names_of.prototype { + let c = self.class_for_fn(f); + let pa = self.heap[c].proto_abs; + let t = TypeSet::obj_one(pa); + out.join_from(&t, &self.engine.abs_labels, &mut self.engine.sink); + } else { + let fo = self.intern_fn_obj(f); + let cell = self.seeded_field_cell(fo, name); + let v = self.engine.read(cell, user); + out.join_from(&v, &self.engine.abs_labels, &mut self.engine.sink); + } + } + } + match rts.obj { + ObjType::Empty => { + // An `unknown`-flagged receiver with no object component is + // "something got here, contents unknown", not "no receiver": + // the read yields the same unknown witness a read through an + // AnyObject receiver does. Without this arm the read would + // contribute nothing and a whole dataflow chain behind one + // unknown-typed receiver would read as empty evidence. + if rts.unknown { + let mut any = TypeSet::unresolved(); + any.fns = Default::default(); + any.obj = ObjType::Empty; + let _ = self.engine.join_ts(out, &any); + } + } + ObjType::One(a) => { + let own = self.seeded_field_cell(a, name); + self.read_join(own, user, out); + if let Some(c) = self.heap[a].class { + // Deliberately not the region root: a precise One + // receiver must read its own class's cells, not the + // merged set's. + let cf = self.class_field_cell(c, name); + self.read_join(cf, user, out); + self.accessor_read(c, name, user, out); + } + if chain_ok { + self.chain_join(a, name, user, out); + } + } + ObjType::ClassAny(c) => { + let c = self.view_class(c, is_elems); + let view = self.engine.cell(CellKey::ClassView { class: c, name }); + let v = self.engine.read(view, user); + let _ = self.engine.join_ts(out, &v); + self.accessor_read(c, name, user, out); + if chain_ok { + let pa = self.heap[c].proto_abs; + self.ensure_seeded(pa); + let f = self.field_cell(pa, name); + let v = self.engine.read(f, user); + out.join_from(&v, &self.engine.abs_labels, &mut self.engine.sink); + self.chain_join(pa, name, user, out); + } + } + ObjType::AnyOf(r) => { + if is_elems { + let union = self.elems_union(); + self.read_join(union, user, out); + } else { + // Some instance of the region's classes: the value is + // unresolved evidence, never fabricated + // definite prim bits; at callee position the fn set is + // the region's method-table union for the name -- the + // flow-scoped upper bound (the classes that actually + // met), never the program-wide name union. + let mut any = TypeSet::unresolved(); + any.fns = Default::default(); + if callee_pos { + let fns = self.region_methods(r, name, user); + if !fns.is_empty() { + region_contributed = true; + } + any.fns = fns; + } + // The region's aggregated view, subscribing the reader. + // The `unknown` witness is still joined alongside it + // (the view is a union of the writes the analysis SAW, + // and writes at an `AnyObject` receiver are dropped by + // design, so it can under-approximate) -- but the + // witness carries NO object component: joined as + // `AnyObject` it absorbed the view's population + // (`join_obj(AnyOf, AnyObject) = AnyObject`), so every + // value read off a region-typed receiver degraded to + // `unk|obj:any` and spread AnyObject through the pool + // and free-list fields it was stored into. The region + // IS the merged fact; `unknown` says the rest honestly. + any.obj = ObjType::Empty; + if let Some(view) = self.region_view(r, name) { + let v = self.engine.read(view, user); + let _ = self.engine.join_ts(out, &v); + } + let _ = self.engine.join_ts(out, &any); + } + } + ObjType::AnyObject => { + if is_elems { + let union = self.elems_union(); + self.read_join(union, user, out); + } else { + let mut any = TypeSet::unresolved(); + any.fns = Default::default(); + let _ = self.engine.join_ts(out, &any); + } + } + } + region_contributed + } + + /// The region's field view for `name`: the ROOT class's view cell, with + /// every member's view linked into it and back out again. + /// + /// One tier up from `class_field_cell`, and the same shape: an + /// abstraction's field cell is linked up into `ClassView`, so a write + /// through a precise receiver is seen by a `ClassAny` read; this links a + /// class's view up into the region's, so a write through a classed + /// receiver is seen by an `AnyOf` read, and back down, so a write at + /// region granularity is seen by class- and alloc-site-level reads. That + /// second direction is the point: without it, a write whose receiver + /// is only known to a region would be dropped outright, emptying the + /// field for every reader. + /// + /// Deliberately NOT a new cell kind. The region root moves as later + /// meets union regions, so a view keyed by the root at creation time + /// would go stale; using the root's own `ClassView` and re-linking + /// lazily on each access makes that self-healing -- after a merge the + /// next access relinks against the new root and member set, and `link` + /// is idempotent, so the repeat costs a hash lookup. + /// + /// Capped like `region_methods`: a mega-region is honestly megamorphic, + /// its union is worth nothing, and the linking is O(members). Past the + /// cap there is no view and the caller keeps the old behaviour. + fn region_view(&mut self, r: ClassId, name: NameId) -> Option { + let root = self.engine.region_root(r); + let members = self + .engine + .region_members + .get(&root) + .cloned() + .unwrap_or_else(|| vec![root]); + if members.len() > crate::constants::REGION_VIEW_CAP { + return None; + } + let view = self.engine.cell(CellKey::ClassView { class: root, name }); + for m in members { + if m == root { + continue; + } + let mv = self.engine.cell(CellKey::ClassView { class: m, name }); + self.engine.link(mv, view); + self.engine.link(view, mv); + } + Some(view) + } + + /// The region's method-table union for `name`: join the fn sets of each + /// member class's proto-abstraction cell (subscribing the reader, so + /// late installs re-fire). Iteration capped -- a huge region is + /// megamorphic and honestly yields nothing. + fn region_methods( + &mut self, + r: ClassId, + name: NameId, + user: (ConId, CtxId), + ) -> super::types::BoundedFnSet { + // Only a small region is a plausible closed dispatch set -- a + // handful of sibling classes all defining the same method. A + // mega-region's method sets are weak guesses whose guard chains + // miss, so it stays honestly megamorphic. + // The REGION cap, not the callee cap: iterating members is O(n) + // once per (region, name) and the resulting fn set is deduped -- + // a region with many sibling classes sharing one method is + // exactly the case the much smaller callee cap would fail to + // resolve, starving every one of that method's arguments. + let cap = crate::constants::REGION_VIEW_CAP; + let root = self.engine.region_root(r); + let members = self + .engine + .region_members + .get(&root) + .cloned() + .unwrap_or_else(|| vec![root]); + let mut fns = super::types::BoundedFnSet::default(); + if members.len() > cap { + return fns; + } + for c in members { + // The member's own prototype AND its chain: a subclass's + // methods usually live on a base prototype, so reading only + // the member's own proto would often yield an empty set -- + // an unresolved call, with no argument flowing into the + // shared method. + let mut cur = self.heap[c].proto_abs; + for _ in 0..CHAIN_DEPTH { + let cell = self.seeded_field_cell(cur, name); + let v = self.engine.read(cell, user); + fns.join_from(&v.fns, &mut self.engine.sink.dropped_fns); + match self.heap[cur].proto { + ProtoLink::Abs(p) => cur = p, + ProtoLink::None => break, + } + } + } + fns + } + + /// Accessor consultation on a classed read: the getter's return joins + /// the result (subscribing, so late getter evidence re-fires). + fn accessor_read(&mut self, c: ClassId, name: NameId, user: (ConId, CtxId), out: &mut TypeSet) { + if let Some(&(Some(g), _)) = self.accessors.get(&(c, name)) { + let r = self.engine.cell(CellKey::Ret { + script: g, + ctx: CTX0, + }); + let v = self.engine.read(r, user); + let _ = self.engine.join_ts(out, &v); + } + } + + /// Accessor consultation on a classed write: the stored value binds + /// the setter's first formal at the generic context. + fn accessor_write(&mut self, c: ClassId, name: NameId, v: &TypeSet, user: (ConId, CtxId)) { + if let Some(&(_, Some(s))) = self.accessors.get(&(c, name)) { + let dst = self.engine.cell(CellKey::Arg { + script: s, + arg: FormalIndex::new(0), + ctx: CTX0, + }); + self.engine.raise(dst, v, user); + } + } + + /// Per-ctx eval tracer for one read site (`NIGHT_TRACE_SITE=:`): + /// every evaluation, with the evaluating ctx, the recv key, and what + /// resolve() handed that ctx -- the instrument for a site whose cells + /// know a class the read evaluations never see. + fn trace_site_eval( + &self, + sid: ScriptId, + pc: Pc, + ctx: CtxId, + recv: super::engine::CKey, + cell: super::engine::CellId, + rts: &TypeSet, + ) { + let Some(site) = super::trace_site_want() else { + return; + }; + if site != Site::new(sid, pc) { + return; + } + crate::diag_line!( + "night: tracesite eval {site} ctx {} recv {:?} cell {} obj {:?} prims {:?} unknown {}", + ctx.0, + recv, + cell.0, + rts.obj, + rts.prims, + rts.unknown + ); + } + + /// Debug tracer for one field name (`NIGHT_TRACE_FIELD=`): every + /// read and write evaluation, with the receiver's abstract object type + /// and, for a `One` receiver, whether that abstraction carries a class. + /// The question it answers is where a field's value stops flowing -- + /// a write through an unclassed `One` never reaches the class view that + /// a `ClassAny` read consults. + fn trace_field( + &self, + tag: &str, + name: NameId, + rts: &TypeSet, + v: Option<&TypeSet>, + user: (ConId, CtxId), + ) { + let Some(want) = super::tracers().field.as_ref() else { + return; + }; + if String::from_utf16_lossy(self.names.get(name)) != *want { + return; + } + let o = match rts.obj { + ObjType::Empty => "Empty".to_string(), + ObjType::One(a) => format!( + "One(abs{} class {:?} snap {})", + a.0, + self.heap[a].class.map(|c| c.0), + u8::from(matches!(self.heap[a].key, AbsKey::Snap(_))) + ), + ObjType::ClassAny(c) => format!("ClassAny({})", c.0), + ObjType::AnyOf(r) => format!("AnyOf({})", r.0), + ObjType::AnyObject => "AnyObject".to_string(), + }; + let sid = self.engine.con_script[user.0 .0 as usize]; + let pc = match &self.engine.cons[user.0 .0 as usize] { + super::engine::Constraint::Read { pc, .. } + | super::engine::Constraint::Write { pc, .. } => Some(*pc), + _ => None, + }; + crate::diag_line!( + "night: tracefield {tag} at {}:{} ctx {} recv {o} val {}", + sid.get(), + pc.map_or(0, |p| p.get()), + user.1 .0, + v.map_or_else(|| "-".to_string(), |t| format!("{:?}", t.obj)) + ); + } + + fn write_into( + &mut self, + rts: &TypeSet, + name: NameId, + v: &TypeSet, + this_recv: bool, + user: (ConId, CtxId), + ) { + self.trace_field("write", name, rts, Some(v), user); + let is_elems = name == self.names_of.elems; + if rts.fns.is_multi() { + self.do_escape(v, user); + } else { + for f in rts.fns.scripted() { + if name == self.names_of.prototype { + // Prototype install: Never value flow -- the concrete + // object becomes a method-table source and chain link. + // If the object already names a class (the shared-ctor + // presolve keys per-prototype classes for scripts many + // classes share), that class owns the table: keying by + // the fn script would home every such class's methods + // to ONE script-keyed class and demote every pin. + if let ObjType::One(x) = v.obj { + let proto_cls = match self.heap[x].key { + AbsKey::Snap(oid) => { + self.heap.class_ids.get(&ClassKey::Proto(oid)).copied() + } + _ => None, + }; + match proto_cls { + Some(c) => self.register_proto_source(c, x), + // A shared ctor script's fn-keyed class serves + // one purpose: `SharedCtor.prototype` reads + // resolve names against it, so it wants the + // union of every sharing class's table -- but + // it must not OWN any of them (homing all their + // methods to the one script class demotes every + // per-prototype pin). + None if self.shared_ctor_sites.values().any(|s| s.ctor == f) => { + let c = self.class_for_fn(f); + self.link_proto_table(c, x); + } + None => { + let c = self.class_for_fn(f); + self.register_proto_source(c, x); + } + } + } + } else { + let fo = self.intern_fn_obj(f); + let cell = self.field_cell(fo, name); + self.engine.raise(cell, v, user); + } + } + } + match rts.obj { + ObjType::Empty => { + // Same rule as the read side: an unknown receiver may hold + // any object the analysis has seen escape, so the written + // value escapes too (and is counted with the drops). + if rts.unknown { + self.stats.dropped_writes += 1; + if this_recv { + self.stats.dropped_this_writes += 1; + } + self.do_escape(v, user); + } + } + ObjType::One(a) => { + let own = self.seeded_field_cell(a, name); + self.engine.raise(own, v, user); + if let Some(owner) = self.heap[a].owner_class { + self.note_method_home(owner, v); + } + if let Some(c) = self.heap[a].class { + self.accessor_write(c, name, v, user); + } + } + ObjType::ClassAny(c) => { + let c = self.view_class(c, is_elems); + let cf = self.class_field_cell(c, name); + self.engine.raise(cf, v, user); + self.accessor_write(c, name, v, user); + } + ObjType::AnyOf(r) if !is_elems => { + // A write whose receiver is known to a region: raise it into + // the region's view, which is linked down into every + // member's class view, so class- and alloc-site-level reads + // see it. Dropping this was what emptied a field for every + // reader when one write site lost its receiver class. + // + // `AnyObject` deliberately keeps the drop (below): it is not + // a bounded set of classes that met, it is everything, and + // distributing a write to every object in the program would + // pollute far more than it recovers. + match self.region_view(r, name) { + Some(view) => self.engine.raise(view, v, user), + None => { + self.stats.dropped_writes += 1; + if this_recv { + self.stats.dropped_this_writes += 1; + } + } + } + self.do_escape(v, user); + } + ObjType::AnyOf(_) | ObjType::AnyObject => { + if is_elems { + let union = self.elems_union(); + self.engine.raise(union, v, user); + } else { + self.stats.dropped_writes += 1; + if this_recv { + self.stats.dropped_this_writes += 1; + } + } + self.do_escape(v, user); + } + } + } + + /// The typed-array kind every object of `obj` has, when one does. + /// A region whose every member is the same typed-array kind still + /// names the kind (pdfjs: distinct Uint8Array alloc sites joined + /// through the DecodeStream buffer field). The consumer arm guards the + /// class at runtime, so this is a prediction like the exact forms, not + /// a proof. + pub(super) fn obj_ta_kind(&self, obj: ObjType) -> Option { + match obj { + ObjType::One(a) => self.heap[a].ta_kind, + ObjType::ClassAny(c) => self.heap[c].ta_kind, + ObjType::AnyOf(r) => { + let root = self.engine.region_root(r); + self.engine + .region_members + .get(&root) + .filter(|ms| ms.len() <= crate::constants::REGION_VIEW_CAP) + .and_then(|ms| { + let mut it = ms.iter().map(|&m| self.heap[m].ta_kind); + let first = it.next().flatten()?; + it.all(|t| t == Some(first)).then_some(first) + }) + } + _ => None, + } + } + + /// The class a typeset contributes to a site's agreement, or `None` + /// when its object half is still EMPTY -- whatever the `unknown` flag + /// says. The flag is not evidence about the class: a region read + /// raises its unknown witness before the region view delivers the + /// object half, so the first evaluations of a site see `Empty|unknown` + /// and only later ones see the class. `Agreed::Conflict` is sticky, so + /// counting that transient as a conflict would poison the site for + /// every later evaluation. A receiver that stays empty contributes + /// nothing and gets no class fact; `AnyObject` still conflicts. + fn site_class_evidence(&self, ts: &TypeSet, regions: RegionLabels) -> Option> { + if ts.obj == ObjType::Empty { + return None; + } + self.recv_class(ts.obj, ts.unknown, regions) + } + + /// Per-site emission evidence: receiver class agreement, elem TA kind, + /// and (reads) the joined result typeset. + fn note_site_evidence( + &mut self, + sid: ScriptId, + pc: Pc, + name: NameId, + rts: &TypeSet, + out: Option<&TypeSet>, + ) { + let site = Site::new(sid, pc); + if let Some(out) = out { + if !out.is_empty() { + let mut t = self.site_read_ts.remove(&site).unwrap_or_default(); + let _ = self.engine.join_ts(&mut t, out); + self.site_read_ts.insert(site, t); + } + // Value-class agreement: what CLASS of object this read + // yields, when every evaluation agrees (AnyObject or a + // class-less abstraction poisons, exactly as the receiver + // agreement does; region labels are accepted). + if let Some(c) = self.site_class_evidence(out, RegionLabels::Accept) { + let e = self.site_value_class.entry(site).or_default(); + match c { + Some(c) => e.observe(c), + None => *e = Agreed::Conflict, + } + } + } + if let Some(c) = self.site_class_evidence(rts, RegionLabels::Refuse) { + let e = self.site_recv_class.entry(site).or_default(); + match c { + Some(c) => e.observe(c), + None => *e = Agreed::Conflict, + } + if Self::unresolved_recv(rts) { + self.site_recv_unresolved.insert(site); + } + } + if let Some(label) = self.site_class_evidence(rts, RegionLabels::Accept) { + let e = self.site_recv_labels.entry(site).or_default(); + let before_conflict = *e == super::types::AgreedSet::Conflict; + match label { + None => e.conflict(), + Some(c) => e.observe(c, RECV_LABEL_CAP), + } + if !before_conflict && *e == super::types::AgreedSet::Conflict && label.is_some() { + self.stats.caps.recv_labels += 1; + } + } + if name == self.names_of.elems { + if let Some(ta) = self.obj_ta_kind(rts.obj) { + observe(&mut self.site_recv_ta, site, ta); + } + } + } + + /// The `TypeSet::unresolved` receiver shape: an object the analysis + /// could not name, no named class beside it. + fn unresolved_recv(ts: &TypeSet) -> bool { + ts.obj == ObjType::AnyObject && ts.unknown + } + + /// Re-derive the receiver-class agreement of every site an unresolved + /// receiver reached, from the FINAL state of each live context, with + /// that row weighed as no evidence: the site's claim is guarded at + /// runtime, and the row is the generic context of a chain unresolved + /// for reasons unrelated to the contexts that named the class. Final + /// states only -- a class observed mid-fixpoint in a context that ends + /// unresolved is a transient, and agreeing on it emits a typed read + /// whose miss departs the track. + pub(super) fn settle_unresolved_recv_sites(&mut self) { + use super::engine::CellKey; + let sites: Vec = self.site_recv_unresolved.iter().copied().collect(); + for site in sites { + let sid = site.script; + let recv = self.engine.script_cons.get(&sid).and_then(|cons| { + cons.iter() + .find_map(|&c| match &self.engine.cons[c.0 as usize] { + Constraint::Read { recv, pc, .. } | Constraint::Write { recv, pc, .. } + if *pc == site.pc => + { + Some(*recv) + } + _ => None, + }) + }); + let Some(recv) = recv else { continue }; + let ctxs = self.engine.live_ctxs.get(&sid).cloned().unwrap_or_default(); + let mut agreed = Agreed::Unset; + for ctx in ctxs { + let key = match recv { + super::engine::CKey::This => CellKey::This { script: sid, ctx }, + _ => { + let id = self.engine.resolve(sid, ctx, recv); + let ts = self.engine.ts(id).clone(); + Self::observe_final_recv(&mut agreed, &ts, self); + continue; + } + }; + if let Some(id) = self.engine.lookup(key) { + let ts = self.engine.ts(id).clone(); + Self::observe_final_recv(&mut agreed, &ts, self); + } + } + self.site_recv_class.insert(site, agreed); + } + } + + fn observe_final_recv(agreed: &mut Agreed, ts: &TypeSet, sv: &Self) { + if Self::unresolved_recv(ts) { + return; + } + match sv.site_class_evidence(ts, RegionLabels::Refuse) { + None => {} + Some(Some(c)) => agreed.observe(c), + Some(None) => *agreed = Agreed::Conflict, + } + } + + /// The class a receiver typeset names, as the per-site channels record + /// it. + /// + /// Three answers, not two: `None` means the receiver contributed no + /// evidence at all (it holds no object yet), `Some(None)` means it held + /// an object the analysis cannot name, and `Some(Some(c))` names it. + /// The middle answer is what conflicts a site, so collapsing it into + /// the first would make an unnamed receiver look like no receiver. + pub(super) fn recv_class( + &self, + o: ObjType, + unknown: bool, + regions: RegionLabels, + ) -> Option> { + Some(match o { + ObjType::Empty if unknown => None, + ObjType::Empty => return None, + ObjType::One(a) => self.heap[a].class, + ObjType::ClassAny(c) => Some(c), + ObjType::AnyOf(r) => match regions { + RegionLabels::Accept => Some(r), + RegionLabels::Refuse => None, + }, + ObjType::AnyObject => None, + }) + } + + /// Receiver-kind census per read site: record the least precise + /// receiver this site has been evaluated with (see [`RecvKind`]). + fn note_site_recv(&mut self, script: ScriptId, pc: Pc, rts: &TypeSet) { + let kind = match rts.obj { + ObjType::Empty => RecvKind::Empty, + ObjType::One(_) => RecvKind::One, + ObjType::ClassAny(_) => RecvKind::ClassAny, + ObjType::AnyOf(_) => RecvKind::AnyOf, + ObjType::AnyObject => RecvKind::AnyObject, + }; + let e = self + .site_recv + .entry(Site::new(script, pc)) + .or_insert(RecvKind::Empty); + if kind > *e { + *e = kind; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::likelier::engine::CKey; + use crate::likelier::Solver; + + fn empty_source() -> Source { + Source { + objects: Vec::new(), + global_object: None, + selfhosted: Vec::new(), + regex_programs: Vec::new(), + } + } + + fn run(sv: &mut Solver<'_>) { + for sid in crate::likelier::sorted_keys(&sv.engine.script_cons) { + sv.engine.instantiate(sid, CTX0); + } + while let Some((c, ctx)) = sv.engine.pop() { + if sv.engine.eval_core(c, ctx) { + continue; + } + if sv.eval_heap(c, ctx) { + continue; + } + assert!(sv.eval_call(c, ctx), "unhandled constraint in heap test"); + } + } + + fn var_ts(sv: &Solver<'_>, script: u32, var: u32) -> TypeSet { + sv.engine + .lookup(CellKey::Var { + script: ScriptId::new(script), + var: VarId::new(var), + ctx: CTX0, + }) + .map(|c| sv.engine.ts(c).clone()) + .unwrap_or_default() + } + + /// The snapshot never-merge principle, structurally: sibling instances + /// of one class do not share field cells (a One read must not see the + /// sibling's value), while the ClassAny view sees all of them. + #[test] + fn sibling_isolation_and_class_view() { + let source = empty_source(); + let gn = HashMap::default(); + let opts = crate::options::Options::default(); + let mut sv = Solver::new(&source, &gn, &opts, crate::ids::Names::default()); + let c = sv.class_for_fn(ScriptId::new(500)); + let a = sv.intern_alloc(ScriptId::new(1), Pc::new(10), CTX0, Some(c), false, None); + let b = sv.intern_alloc(ScriptId::new(1), Pc::new(20), CTX0, Some(c), false, None); + let n = sv.names.intern(&['f' as u16]); + let mk = |sv: &mut Solver<'_>, con| { + sv.engine.add_con(ScriptId::new(1), con); + }; + mk( + &mut sv, + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::obj_one(a), + }, + ); + mk( + &mut sv, + Constraint::Const { + dst: CKey::Var(VarId::new(1)), + ts: TypeSet::prim(PRIM_INT32), + }, + ); + mk( + &mut sv, + Constraint::Write { + recv: CKey::Var(VarId::new(0)), + name: n, + src: CKey::Var(VarId::new(1)), + pc: Pc::new(0), + }, + ); + mk( + &mut sv, + Constraint::Const { + dst: CKey::Var(VarId::new(2)), + ts: TypeSet::obj_one(b), + }, + ); + mk( + &mut sv, + Constraint::Read { + recv: CKey::Var(VarId::new(2)), + name: n, + dst: CKey::Var(VarId::new(3)), + pc: Pc::new(4), + callee_pos: false, + }, + ); + // A joined receiver (a join b = ClassAny(c)) reads the class view. + mk( + &mut sv, + Constraint::Const { + dst: CKey::Var(VarId::new(4)), + ts: TypeSet::obj_one(a), + }, + ); + mk( + &mut sv, + Constraint::Const { + dst: CKey::Var(VarId::new(4)), + ts: TypeSet::obj_one(b), + }, + ); + mk( + &mut sv, + Constraint::Read { + recv: CKey::Var(VarId::new(4)), + name: n, + dst: CKey::Var(VarId::new(5)), + pc: Pc::new(8), + callee_pos: false, + }, + ); + run(&mut sv); + // Sibling b sees nothing of a's own write... + assert!(var_ts(&sv, 1, 3).prims.is_empty()); + // ...but the receiver itself joined to ClassAny... + let recv = var_ts(&sv, 1, 4); + assert_eq!(recv.obj, ObjType::ClassAny(c)); + // ...and the ClassAny read sees the instance write through the view. + assert_eq!(var_ts(&sv, 1, 5).prims, PRIM_INT32); + } + + /// Writes through a ClassAny receiver land in ClassField and are seen + /// by One readers of any member (the ClassField/ClassView split). + #[test] + fn class_field_reaches_one_readers() { + let source = empty_source(); + let gn = HashMap::default(); + let opts = crate::options::Options::default(); + let mut sv = Solver::new(&source, &gn, &opts, crate::ids::Names::default()); + let c = sv.class_for_fn(ScriptId::new(501)); + let a = sv.intern_alloc(ScriptId::new(1), Pc::new(10), CTX0, Some(c), false, None); + let b = sv.intern_alloc(ScriptId::new(1), Pc::new(20), CTX0, Some(c), false, None); + let n = sv.names.intern(&['g' as u16]); + for con in [ + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::obj_one(a), + }, + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::obj_one(b), + }, + Constraint::Const { + dst: CKey::Var(VarId::new(1)), + ts: TypeSet::prim(PRIM_DOUBLE), + }, + Constraint::Write { + recv: CKey::Var(VarId::new(0)), + name: n, + src: CKey::Var(VarId::new(1)), + pc: Pc::new(0), + }, + Constraint::Const { + dst: CKey::Var(VarId::new(2)), + ts: TypeSet::obj_one(a), + }, + Constraint::Read { + recv: CKey::Var(VarId::new(2)), + name: n, + dst: CKey::Var(VarId::new(3)), + pc: Pc::new(4), + callee_pos: false, + }, + ] { + sv.engine.add_con(ScriptId::new(1), con); + } + run(&mut sv); + assert_eq!(var_ts(&sv, 1, 3).prims, PRIM_DOUBLE); + } + + /// The region rung end to end, in miniature: two + /// instances of different classes meet (-> AnyOf(region)); a + /// callee-position read of a method name resolves the region's + /// method-table union; the call site emits the flow-scoped set. + #[test] + fn region_method_resolution() { + let source = empty_source(); + let gn = HashMap::default(); + let opts = crate::options::Options::default(); + let mut sv = Solver::new(&source, &gn, &opts, crate::ids::Names::default()); + let c1 = sv.class_for_fn(ScriptId::new(700)); + let c2 = sv.class_for_fn(ScriptId::new(701)); + let a = sv.intern_alloc(ScriptId::new(1), Pc::new(10), CTX0, Some(c1), false, None); + let b = sv.intern_alloc(ScriptId::new(1), Pc::new(20), CTX0, Some(c2), false, None); + let m = sv.names.intern(&['m' as u16]); + let proto_name = sv.names_of.prototype; + for (fscript, method, base) in [(700u32, 800u32, 0u32), (701, 801, 10)] { + sv.engine.add_con( + ScriptId::new(2), + Constraint::Const { + dst: CKey::Var(VarId::new(base)), + ts: TypeSet::fn_one(FnId::script(ScriptId::new(fscript))), + }, + ); + sv.engine.add_con( + ScriptId::new(2), + Constraint::Read { + recv: CKey::Var(VarId::new(base)), + name: proto_name, + dst: CKey::Var(VarId::new(base + 1)), + pc: Pc::new(base), + callee_pos: false, + }, + ); + sv.engine.add_con( + ScriptId::new(2), + Constraint::Const { + dst: CKey::Var(VarId::new(base + 2)), + ts: TypeSet::fn_one(FnId::script(ScriptId::new(method))), + }, + ); + sv.engine.add_con( + ScriptId::new(2), + Constraint::Write { + recv: CKey::Var(VarId::new(base + 1)), + name: m, + src: CKey::Var(VarId::new(base + 2)), + pc: Pc::new(base + 1), + }, + ); + } + // The meet, and the dispatch through it. + sv.engine.add_con( + ScriptId::new(1), + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::obj_one(a), + }, + ); + sv.engine.add_con( + ScriptId::new(1), + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::obj_one(b), + }, + ); + sv.engine.add_con( + ScriptId::new(1), + Constraint::Read { + recv: CKey::Var(VarId::new(0)), + name: m, + dst: CKey::Var(VarId::new(1)), + pc: Pc::new(5), + callee_pos: true, + }, + ); + sv.engine.add_con( + ScriptId::new(1), + Constraint::Call { + callee: CKey::Var(VarId::new(1)), + this_: Some(CKey::Var(VarId::new(0))), + args: Vec::new().into(), + ret: CKey::Var(VarId::new(2)), + pc: Pc::new(9), + construct: false, + }, + ); + run(&mut sv); + let recv = var_ts(&sv, 1, 0); + assert!(matches!(recv.obj, ObjType::AnyOf(_)), "meet -> {recv:?}"); + let callee = var_ts(&sv, 1, 1); + assert_eq!( + callee.fns.ids(), + &[ + FnId::script(ScriptId::new(800)), + FnId::script(ScriptId::new(801)) + ] + ); + let site = sv.site_calls.get(&Site::from_raw(1, 9)).expect("site fact"); + assert_eq!( + site.ids(), + &[ + FnId::script(ScriptId::new(800)), + FnId::script(ScriptId::new(801)) + ] + ); + } + + /// A method read that dead-ends before the prototype is installed + /// re-fires when `F.prototype = {...; m: fn}` lands later (sentinel + + /// standing links), and the method-home attribution records the ctor. + #[test] + fn late_prototype_install_refires() { + let source = empty_source(); + let gn = HashMap::default(); + let opts = crate::options::Options::default(); + let mut sv = Solver::new(&source, &gn, &opts, crate::ids::Names::default()); + let c = sv.class_for_fn(ScriptId::new(600)); + let inst = sv.intern_alloc(ScriptId::new(1), Pc::new(10), CTX0, Some(c), false, None); + let m = sv.names.intern(&['m' as u16]); + let proto_name = sv.names_of.prototype; + // Script 1: read inst.m (evaluates first, dead-ends). + sv.engine.add_con( + ScriptId::new(1), + Constraint::Const { + dst: CKey::Var(VarId::new(0)), + ts: TypeSet::obj_one(inst), + }, + ); + sv.engine.add_con( + ScriptId::new(1), + Constraint::Read { + recv: CKey::Var(VarId::new(0)), + name: m, + dst: CKey::Var(VarId::new(1)), + pc: Pc::new(0), + callee_pos: false, + }, + ); + run(&mut sv); + assert!(var_ts(&sv, 1, 1).fns.is_empty()); + // Script 2: F.prototype = lit; lit.m = + + + +""" + + +def default_compiler(): + """The compiler binary to drive: an explicit override, else whichever + canonical build output exists.""" + env = os.environ.get("NIGHTMONKEY") + if env: + return env + repo = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") + ) + for rel in ( + "obj-nightmonkey-inprocess/dist/host/bin/nightmonkey", + "obj-nightmonkey/dist/host/bin/nightmonkey", + "js/src/night/nightmonkey/target/release/nightmonkey", + ): + cand = os.path.join(repo, rel) + if os.path.exists(cand): + return cand + return "nightmonkey" + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("snapshot") + ap.add_argument("source") + ap.add_argument("-o", "--out", required=True) + ap.add_argument( + "--nmc", + default=default_compiler(), + help="the nightmonkey compiler binary (default: the first of " + "$NIGHTMONKEY, the objdir dist/host/bin builds, or target/release)", + ) + ap.add_argument( + "--stderr-cache", + help="reuse/store the compiler stderr dump at this path", + ) + ap.add_argument( + "--no-lower", + action="store_true", + help="skip the per-op LOWERING (--viz-lower), which is on by " + "default: the mini-CFG each op expands to, with guards, boxing, " + "memory kinds, helper calls and continuations. It is " + "per-instruction data, so drop it when the page gets unwieldy -- " + "crypto goes 5M -> 22M and the biggest bundles are far worse.", + ) + # Accepted and ignored: lowering used to be opt-in via this flag. + ap.add_argument("--lower", action="store_true", help=argparse.SUPPRESS) + ap.add_argument("--title") + args = ap.parse_args() + + if args.stderr_cache and os.path.exists(args.stderr_cache): + text = open(args.stderr_cache, encoding="utf-8", errors="replace").read() + else: + text = run_compiler(args.nmc, args.snapshot, not args.no_lower) + if args.stderr_cache: + with open(args.stderr_cache, "w", encoding="utf-8") as f: + f.write(text) + + scripts, layouts, arrclaims, helpers = parse_dump(text) + if not scripts: + raise SystemExit("no viz records in compiler output (is --viz plumbed?)") + source_text = open(args.source, encoding="utf-8", errors="replace").read() + anchor_scripts(scripts, source_text) + title = args.title or ( + "NightMonkey speculation: " + os.path.basename(args.snapshot) + ) + page = build_html( + scripts, + layouts, + arrclaims, + helpers, + source_text, + os.path.basename(args.source), + title, + ) + with open(args.out, "w", encoding="utf-8") as f: + f.write(page) + nver = sum(len(s["vers"]) for s in scripts.values()) + nlow = sum(len(s["lower"]) for s in scripts.values()) + print( + f"wrote {args.out}: {len(scripts)} scripts, {nver} versions, " + f"{sum(len(s['ops']) for s in scripts.values())} ops" + + (f", {nlow} lowered ops" if nlow else "") + ) + + +if __name__ == "__main__": + main() diff --git a/js/src/night/wasm-jit-runner/.gitignore b/js/src/night/wasm-jit-runner/.gitignore new file mode 100644 index 0000000000000..1b63c34f403e1 --- /dev/null +++ b/js/src/night/wasm-jit-runner/.gitignore @@ -0,0 +1,2 @@ +/target +/guest/example/test_guest.wasm diff --git a/js/src/night/wasm-jit-runner/Cargo.lock b/js/src/night/wasm-jit-runner/Cargo.lock new file mode 100644 index 0000000000000..6ba28dd23e4ab --- /dev/null +++ b/js/src/night/wasm-jit-runner/Cargo.lock @@ -0,0 +1,2493 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix", + "winx", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc293b86236abcc45f2f72e2d18e2bd636f2a08b75eb286bae31e71e1430c91" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b954c826eddaf1b001402cb8aecf1764c6f6d637ba69fb9e3311f1ebac965be6" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4053fa2575ef4a5c35d2708533df2200400ae979226cea9cc92a578b811bd4e7" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d216663191014aa63e1d2cffd058e609eaf207646d40b739d88250f65b2c4f69" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5e7e7aad6a425a51da1ad7ab9e5d280ea97eb7c7c4545fafb567915a75aadb" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c421d80a9a85f806cb02a2983b5b5368a335c319795b1f1b4b771a24479af5b0" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78fdb83ab012d0ee6a44ced7ca8788a444f17cf821c62f95d6ef87c9f0262518" + +[[package]] +name = "cranelift-control" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b75adc6eb7bb4ac6365106afb6cac4f12fe1ddfa02ddc9fd7015ca1469b471b" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668e56db75a54816cbdd7c7b7bfc558b08bf7b2cda9d0846491517e92f3b393b" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63892dc1cc3ae48680183fa66997f60ffe7f1e200c8d390f8ee66edff4aef5a" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94eaf429c32a12715429c7c6ddfdd43c170f4cdd7e97bfa507bd68a652091087" + +[[package]] +name = "cranelift-native" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd77674904ae9be11c1e1efdba54788b59f3d6658d747b97534bfbba2909aacc" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.132.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba7c0ff5941842c36653da155580ce41e675c204a67ac1b4e1c478a9347bbb7" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9880c1985ccccaed3646b0ef793dc39a4b117403ed4afc6fa3ef6027c5200f" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee249346855ad102580e474da5463f86f8a7d449e6d49e00fefb304e448e2983" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba953e2b9b4b4b52a31cf4e3ee1c1374c872b6e012cf2138d1c37cba00bfd6" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6" +dependencies = [ + "leb128fmt", + "wasmparser 0.248.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +dependencies = [ + "leb128fmt", + "wasmparser 0.252.0", +] + +[[package]] +name = "wasm-jit-runner" +version = "0.1.0" +dependencies = [ + "anyhow", + "sha2", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "wasmparser" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b264a5410b008d4d199a92bf536eae703cbd614482fc1ec53831cf19e1c183" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.248.0", +] + +[[package]] +name = "wasmtime" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ce9aa2c67f75fadcfdc6aa9097d03e7c39485dfe316f2ed6a7c0fd186c527" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fb157bd1fbf689ac89d570433a700db6f33bdfcb5ffc30e3f1c49e4c70de71" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d1a46c4a2360186b59c6ed7a74a1121ac97925ae9a18db1b2f146cc27ac0b7" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96c17f35fae2ab574667aba0c58fd56349a6f788ac42541a2e543116d5cfb91" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2eeb9b53222859e6f5dc73d2ccfb33254d672469cac11b693a71912e2f3817" + +[[package]] +name = "wasmtime-internal-core" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1deaf6bc3430abd7497b00c64f06ca2b97ca0fe41af87836446ca30949965c" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845f83b5b04b11bc48329b53eb4fa8cf9f28a43c71ed8e1203f68ffa9806d1b" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.248.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10c8466f72965ae85c250f90aaa7992c089a2f8502009bd0d2c9e7d6409174a" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3adfecf5621b14d8f8871f4cb4ed9f844197b1ddefc702ef4c859552cd9551" +dependencies = [ + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d3c1e9fb618ec45c9b3477ea683cd37bee427273d7b13bba5c66a1caaf1dd6" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa91132b81f1e172ec7e7c3c114ac34209ee6b3524b3a8d6943af99803f66c5" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea811ffe23f597cc7708327ea25d9eb018dcf760ffe15ccb7d0b27ad635de61" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828b66175c54a0d00b4c1c1c76658d8aa73aeb9fa3553575c5eee56d40f2eb18" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.248.0", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae00896ad9bef1b3ca6401ae9a841daa6f357dd91541b6baf87082946d1bde1" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wasmtime-wasi" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032ceffcb74cf30a2cdac298a4d6ce219058f08479ce1ab38434aadc044000b" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-std", + "cap-time-ext", + "cfg-if", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rand", + "rustix", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b6e6868e5b93e1e10983a17afb631b39c236d8b6b4abe9faffe78f1ee0c6e7" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "252.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.252.0", +] + +[[package]] +name = "wat" +version = "1.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" +dependencies = [ + "wast 252.0.0", +] + +[[package]] +name = "wiggle" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176527a028d6a426a514e3ca650c251a60541cde26df421f781339f27553ff9f" +dependencies = [ + "bitflags", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604976b16d40f15606ae47ca22473c7574b317a6445ea2e3986f834a2ca0f449" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7252f1689c33cf77cfac6115047c6a8b53f188c25c644f7856ad66c881c4077" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winch-codegen" +version = "45.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c09acfdfa281b3340e1e94ef3cf6618d69eab975280f881e154c29f49419c1" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.248.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-parser" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247ad505da2915a082fe13204c5ba8788425aea1de54f43b284818cf82637856" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.248.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/js/src/night/wasm-jit-runner/Cargo.toml b/js/src/night/wasm-jit-runner/Cargo.toml new file mode 100644 index 0000000000000..d33842dbacd9b --- /dev/null +++ b/js/src/night/wasm-jit-runner/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasm-jit-runner" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +wasmtime = "45" +wasmtime-wasi = "45" +wasm-encoder = { version = "0.252", features = ["wasmparser"] } +wasmparser = "0.252" +sha2 = "0.10" + +[profile.release] +debug = true + +[workspace] diff --git a/js/src/night/wasm-jit-runner/README.md b/js/src/night/wasm-jit-runner/README.md new file mode 100644 index 0000000000000..009a901230e65 --- /dev/null +++ b/js/src/night/wasm-jit-runner/README.md @@ -0,0 +1,127 @@ +# wasm-jit-runner + +A small WASI **preview 1** CLI runner built on the [`wasmtime`](https://crates.io/crates/wasmtime) +crate, with one extra capability: the running guest can **add new wasm functions +to itself at runtime** and call them — no round-trip back out to the runner. + +This is meant for testing a wasm-targeting compiler *in situ*: emit a function, +hand its bytes to the runner, get back a callable funcptr, and invoke it +immediately, all from inside one guest process. + +``` +wasm-jit-runner [guest args...] +``` + +It otherwise behaves as an ordinary wasip1 command runner (inherits stdio/env, +forwards argv, propagates the exit code). + +## The `wasm_add_funcs` API + +The runner injects one host import, `env.wasm_add_funcs`: + +```c +err_t wasm_add_funcs(uint8_t** bytecode, size_t* lens, int nfuncs, funcptr_t* out); +``` + +* `bytecode[i]` / `lens[i]` describe `nfuncs` **function blobs** (see below). +* On success it writes `nfuncs` **funcptrs** (indices into table 0, the + indirect-function-table) into `out` and returns `0`. On failure it returns + non-zero and logs a diagnostic to stderr (the guest keeps running). + +### Semantics + +The supplied functions are assembled into a single fresh core-wasm module and +instantiated into the *same* store, where: + +* there are **no imported functions**, so the new functions call each other + directly by index — function `0` is the first blob you pass; +* the host module's **memories, tables, and globals are imported at their + existing indices**, so new code can reference them directly; +* the host module's **functions are deliberately not visible**. To call back + into existing guest code, do an **indirect call through table 0** (a C + funcptr is exactly a table-0 index, so `&some_func` gives you the index). + +Each new function is appended to table 0, and its slot index is returned as the +funcptr. The guest needs no special linker flags: the runner makes the funcptr +table growable when it loads the module (see below). + +### Function blob format + +Each blob is a wasm functype followed by a wasm code body: + +``` +0x60 ; functype tag +uleb(nparams) param-types ; valtype bytes (0x7f=i32, 0x7e=i64, ...) +uleb(nresults) result-types +uleb(nlocalruns) localruns ; each: uleb(count) valtype + ; instructions, terminated by `end` (0x0b) +``` + +The core API is the small, dependency-free header +[`guest/wasm_add.h`](guest/wasm_add.h) (just the import declaration and types) — +copy it into any project that uses the API. Header-only helpers for *building* +the blobs live separately in [`guest/wasm_build.h`](guest/wasm_build.h). + +## How it works + +1. The guest module is stream-edited (`src/modedit.rs`) to (a) add synthetic + exports for every memory, table, and global, so the runner can get live + handles to them, and (b) strip the maximum off every table type so the + funcptr table can grow (no `-Wl,--growable-table` needed on the guest). +2. On each `wasm_add_funcs` call (`src/addfuncs.rs`) the runner parses the + blobs, reads the live items' types, assembles a new module that imports those + items and defines the new functions, compiles and instantiates it, then grows + table 0 and writes the new functions into it. + +## Building and testing + +```sh +cargo build # build the runner +sh guest/example/build.sh # build the example guest (needs wasi-sdk) +sh test.sh # build everything and run the example +``` + +The example guest ([`guest/example/test_guest.c`](guest/example/test_guest.c)) +builds three functions at runtime and checks: + +* basic computation (`func0(x) = x + 100`); +* a direct call between added functions (`func1` calls `func0` by index); +* an added function that reads parameters, performs an **indirect call back into + an existing guest function** via table 0, and **writes to linear memory** + (`func2`). + +Expected output: + +``` +added 3 functions, funcptrs = 6, 7, 8 +func0(5) = 105 (expect 105) +func1(5) = 106 (expect 106) +func2(helper,7,&sink)= 98 (expect 98) +g_sink = 49 (expect 49) +ALL TESTS PASSED +``` + +The example guest builds with no special linker flags. The wasi-sdk is expected +at `/opt/wasi-sdk` (override the compiler with +`WASI_CC=/path/to/wasm32-wasip1-clang`). + + +## In-tree extensions (js/src/night/wasm-jit-runner) + +This copy is extended from the standalone wasm-jit-runner project for the +SpiderMonkey AOT in-process test flow: + +- `--dir HOST[::GUEST]` preopens (repeatable; default preopens `/`), so the + guest shell can read test files by absolute path. +- `--cache-dir DIR`: content-addressed cwasm cache (sha256 of edited module + bytes + engine compatibility hash); makes repeated runs of a large guest + module start in tens of milliseconds. +- `env.wasm_table_size() -> u32`: current table-0 size. Added functions are + appended contiguously (API guarantee), so a guest can predict the funcptr + of blob i in the next add call as `size + i`. +- `env.wasm_add_funcs2(bytecode, lens, nfuncs, extern_funcs, nextern, out)`: + like `wasm_add_funcs`, but the assembled module also imports `nextern` + functions resolved by the host from the given table-0 indices; they occupy + function indices `0..nextern` so blob code can `call` them directly, and + each import's type is taken from the live table entry (a signature mismatch + fails instantiation loudly). diff --git a/js/src/night/wasm-jit-runner/guest/example/build.sh b/js/src/night/wasm-jit-runner/guest/example/build.sh new file mode 100755 index 0000000000000..515b21b5cbb75 --- /dev/null +++ b/js/src/night/wasm-jit-runner/guest/example/build.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# Build the test guest into a wasip1 wasm module. +# +# No special linker flags are required: the runner stream-edits the module on +# load to export its memories/tables/globals and to make the indirect function +# table growable. +set -e +DIR="$(cd "$(dirname "$0")" && pwd)" +CC="${WASI_CC:-/opt/wasi-sdk/bin/wasm32-wasip1-clang}" + +# The core/builder headers live one directory up. +"$CC" -I"$DIR/.." "$DIR/test_guest.c" -O2 -o "$DIR/test_guest.wasm" + +echo "built $DIR/test_guest.wasm" diff --git a/js/src/night/wasm-jit-runner/guest/example/test_guest.c b/js/src/night/wasm-jit-runner/guest/example/test_guest.c new file mode 100644 index 0000000000000..1224e6ce25fc8 --- /dev/null +++ b/js/src/night/wasm-jit-runner/guest/example/test_guest.c @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +/* + * test_guest.c — exercises the `wasm_add_funcs` runtime-function-adding API. + * + * It dynamically builds three wasm functions and verifies: + * - basic execution + memory-independent computation (func0); + * - one added function directly calling another by index (func1 calls func0); + * - an added function reading parameters, performing an *indirect* call back + * into an existing guest function via table 0, and writing to the guest's + * linear memory (func2). + * + * Build (see build.sh): needs the guest headers on the include path, e.g. + * wasm32-wasip1-clang -I.. test_guest.c -O2 -o test_guest.wasm + */ +#include "wasm_build.h" +#include +#include + +/* + * An ordinary guest function that the dynamically-added code will call back + * into, *indirectly* through table 0. Taking its address forces it into the + * indirect function table; `used`/`noinline` keep it intact under -O2. + */ +__attribute__((noinline, used)) int host_helper(int x) { return x * x; } + +/* Where func2 will store its result; address passed in as a parameter. */ +volatile int g_sink = 0; + +int main(void) { + int failures = 0; + + /* ---- func0: (i32 x) -> i32 ; returns x + 100 ---------------------- */ + wa_func f0; + wa_func_init(&f0); + wa_param(&f0, WA_I32); + wa_result(&f0, WA_I32); + wa_local_get(&f0, 0); /* x */ + wa_i32_const(&f0, 100); /* 100 */ + wa_i32_add(&f0); /* x + 100 */ + wa_end(&f0); + + /* ---- func1: (i32 x) -> i32 ; returns func0(x) + 1 ----------------- */ + /* Demonstrates a *direct* call (by index 0) between added functions. */ + wa_func f1; + wa_func_init(&f1); + wa_param(&f1, WA_I32); + wa_result(&f1, WA_I32); + wa_local_get(&f1, 0); /* x */ + wa_call(&f1, 0); /* call func0 */ + wa_i32_const(&f1, 1); + wa_i32_add(&f1); /* + 1 */ + wa_end(&f1); + + /* + * ---- func2: (i32 helper, i32 x, i32 addr) -> i32 ------------------ + * r = (*helper)(x) ; indirect call through table 0 + * *(i32*)addr = r ; write to guest linear memory + * return r * 2 + * Uses one i32 local (index 3) to hold r. The indirect call uses type + * index 0, whose signature — (i32)->i32 — is func0's type. + */ + wa_func f2; + wa_func_init(&f2); + wa_param(&f2, WA_I32); /* 0: helper funcptr */ + wa_param(&f2, WA_I32); /* 1: x */ + wa_param(&f2, WA_I32); /* 2: addr */ + wa_result(&f2, WA_I32); + wa_local(&f2, WA_I32, 1); /* local 3: r */ + + wa_local_get(&f2, 1); /* x */ + wa_local_get(&f2, 0); /* helper (table index) */ + wa_call_indirect(&f2, 0, 0); /* call_indirect type0 table0 */ + wa_local_set(&f2, 3); /* r = ... */ + wa_local_get(&f2, 2); /* addr */ + wa_local_get(&f2, 3); /* r */ + wa_i32_store(&f2, 2, 0); /* *(i32*)addr = r */ + wa_local_get(&f2, 3); /* r */ + wa_i32_const(&f2, 2); + wa_i32_mul(&f2); /* r * 2 */ + wa_end(&f2); + + wa_func funcs[3] = {f0, f1, f2}; + wa_funcptr ptr[3]; + wa_err err = wa_add_funcs(funcs, 3, ptr); + if (err != 0) { + printf("wasm_add_funcs failed with err=%d\n", err); + return 1; + } + printf("added 3 functions, funcptrs = %d, %d, %d\n", ptr[0], ptr[1], ptr[2]); + + /* Call the freshly-added functions through their funcptrs. */ + int (*fn0)(int) = (int (*)(int))(intptr_t)ptr[0]; + int (*fn1)(int) = (int (*)(int))(intptr_t)ptr[1]; + int (*fn2)(int, int, int) = (int (*)(int, int, int))(intptr_t)ptr[2]; + + int r0 = fn0(5); + printf("func0(5) = %d (expect 105)\n", r0); + failures += (r0 != 105); + + int r1 = fn1(5); + printf("func1(5) = %d (expect 106)\n", r1); + failures += (r1 != 106); + + int helper_idx = (int)(intptr_t)(void*)&host_helper; + int addr = (int)(intptr_t)(void*)&g_sink; + int r2 = fn2(helper_idx, 7, addr); + printf("func2(helper,7,&sink)= %d (expect 98)\n", r2); + printf("g_sink = %d (expect 49)\n", g_sink); + failures += (r2 != 98); + failures += (g_sink != 49); + + /* + * ---- func3 (wasm_add_funcs2): (i32 x) -> i32 ---------------------- + * Direct-calls IMPORTED function 0 (host_helper, resolved by the host + * from its table-0 index) and adds 1000: returns host_helper(x) + 1000. + * Also checks the wasm_table_size index-prediction contract: the blob's + * funcptr must equal the pre-call table size. + */ + int base = wasm_table_size(); + printf("table size = %d (expect > 0)\n", base); + failures += (base <= 0); + + wa_func f3; + wa_func_init(&f3); + wa_param(&f3, WA_I32); + wa_result(&f3, WA_I32); + wa_local_get(&f3, 0); /* x */ + wa_call(&f3, 0); /* call imported helper */ + wa_i32_const(&f3, 1000); + wa_i32_add(&f3); + wa_end(&f3); + + wa_func funcs2[1] = {f3}; + wa_funcptr externs[1] = {(wa_funcptr)(intptr_t)(void*)&host_helper}; + wa_funcptr ptr3[1]; + err = wa_add_funcs2(funcs2, 1, externs, 1, ptr3); + if (err != 0) { + printf("wasm_add_funcs2 failed with err=%d\n", err); + return 1; + } + printf("func3 funcptr = %d (expect %d, predicted)\n", ptr3[0], base); + failures += (ptr3[0] != base); + + int (*fn3)(int) = (int (*)(int))(intptr_t)ptr3[0]; + int r3 = fn3(6); + printf("func3(6) = %d (expect 1036)\n", r3); + failures += (r3 != 1036); + + if (failures == 0) { + printf("ALL TESTS PASSED\n"); + return 0; + } + printf("%d CHECK(S) FAILED\n", failures); + return 1; +} diff --git a/js/src/night/wasm-jit-runner/guest/wasm_add.h b/js/src/night/wasm-jit-runner/guest/wasm_add.h new file mode 100644 index 0000000000000..adbb2a370fbfc --- /dev/null +++ b/js/src/night/wasm-jit-runner/guest/wasm_add.h @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +/* + * wasm_add.h — core API for the `wasm-jit-runner` runtime-function-adding host + * import. This is the minimal, dependency-free header to copy into a project + * that wants to use the API; for convenience helpers that *build* the function + * blobs, see the separate `wasm_build.h`. + * + * The runner exposes one import that lets a running wasm guest assemble + * brand-new wasm functions at runtime and obtain "funcptrs" (entries in table + * 0, the indirect-function-table) that can be called like any other function + * pointer. + * + * wa_err wasm_add_funcs(uint8_t** bytecode, size_t* lens, int nfuncs, + * wa_funcptr* out); + * + * - bytecode[i] / lens[i] describe `nfuncs` "function blobs" (format below). + * - On success the call writes `nfuncs` funcptrs into `out` and returns 0. + * On failure it returns non-zero (and the runner logs a diagnostic); the + * guest keeps running. + * + * Semantics: the supplied functions are assembled into a single fresh module + * and instantiated into the same store, where: + * - there are no imported functions, so the new functions call each other + * directly by index — function 0 is the first blob you pass; + * - the host module's memories, tables and globals are imported at their + * existing indices, so new code can reference them directly; + * - the host module's functions are not visible. To call back into existing + * guest code, do an indirect call through table 0 (a C funcptr is exactly a + * table-0 index, so `&some_func` gives you the index). + * Each new function is appended to table 0; its slot index is the returned + * funcptr. + * + * Function blob format (per function): + * + * 0x60 ; functype tag + * uleb(nparams) param-types ; valtype bytes (0x7f=i32, 0x7e=i64, ...) + * uleb(nresults) result-types + * uleb(nlocalruns) localruns ; each: uleb(count) valtype + * ; instructions, terminated by `end` (0x0b) + */ +#ifndef WASM_ADD_H +#define WASM_ADD_H + +#include +#include + +typedef int wa_err; /* 0 == success */ +typedef int wa_funcptr; /* index into table 0 */ + +extern wa_err wasm_add_funcs(uint8_t** bytecode, size_t* lens, int nfuncs, + wa_funcptr* out) + __attribute__((import_module("env"), import_name("wasm_add_funcs"))); + +/* + * Like wasm_add_funcs, but the assembled module additionally IMPORTS + * `nextern` functions, resolved by the host from table-0 entries + * `extern_funcs[0..nextern)` (a C funcptr is exactly such an index). The + * imported functions occupy the new module's function indices 0..nextern, so + * blob code can `call` them directly; blob i is function index nextern+i. + * Each import's type is taken from the live table entry, so a signature + * mismatch in blob code fails instantiation (loudly) rather than trapping + * later. + */ +extern wa_err wasm_add_funcs2(uint8_t** bytecode, size_t* lens, int nfuncs, + const wa_funcptr* extern_funcs, int nextern, + wa_funcptr* out) + __attribute__((import_module("env"), import_name("wasm_add_funcs2"))); + +/* + * Current size of table 0. Added functions are appended contiguously at the + * end of the table (an API guarantee), so after querying this a guest can + * predict the funcptr of blob i in the next wasm_add_funcs* call as + * `size + i` -- e.g. to bake callee indices into blob code before the call. + * Returns -1 on failure. + */ +extern int wasm_table_size(void) + __attribute__((import_module("env"), import_name("wasm_table_size"))); + +#endif /* WASM_ADD_H */ diff --git a/js/src/night/wasm-jit-runner/guest/wasm_build.h b/js/src/night/wasm-jit-runner/guest/wasm_build.h new file mode 100644 index 0000000000000..a1d6d5ae42a84 --- /dev/null +++ b/js/src/night/wasm-jit-runner/guest/wasm_build.h @@ -0,0 +1,236 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +/* + * wasm_build.h — header-only helpers for *building* the function blobs consumed + * by `wasm_add_funcs` (see `wasm_add.h` for the core API). + * + * Provides: + * - a tiny growable byte buffer (`wa_buf`); + * - a `wa_func` builder that emits a single function blob (a wasm functype + * followed by a wasm code body); + * - convenience emitters for common opcodes; and + * - `wa_add_funcs`, which finishes a batch of `wa_func`s and hands them to + * the runner in one call. + * + * This header is optional: a project that produces blobs some other way (e.g. a + * compiler emitting the blob format directly) only needs `wasm_add.h`. + */ +#ifndef WASM_BUILD_H +#define WASM_BUILD_H + +#include "wasm_add.h" + +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* valtypes */ +/* ------------------------------------------------------------------ */ + +typedef enum { + WA_I32 = 0x7f, + WA_I64 = 0x7e, + WA_F32 = 0x7d, + WA_F64 = 0x7c, + WA_FUNCREF = 0x70, + WA_EXTERNREF = 0x6f, +} wa_valtype; + +/* ------------------------------------------------------------------ */ +/* Growable byte buffer */ +/* ------------------------------------------------------------------ */ + +typedef struct { + uint8_t* data; + size_t len; + size_t cap; +} wa_buf; + +static inline void wa_buf_reserve(wa_buf* b, size_t extra) { + if (b->len + extra <= b->cap) return; + size_t cap = b->cap ? b->cap : 16; + while (cap < b->len + extra) cap *= 2; + b->data = (uint8_t*)realloc(b->data, cap); + b->cap = cap; +} + +static inline void wa_buf_u8(wa_buf* b, uint8_t x) { + wa_buf_reserve(b, 1); + b->data[b->len++] = x; +} + +static inline void wa_buf_bytes(wa_buf* b, const void* p, size_t n) { + wa_buf_reserve(b, n); + memcpy(b->data + b->len, p, n); + b->len += n; +} + +static inline void wa_buf_uleb(wa_buf* b, uint64_t v) { + do { + uint8_t byte = v & 0x7f; + v >>= 7; + if (v) byte |= 0x80; + wa_buf_u8(b, byte); + } while (v); +} + +static inline void wa_buf_sleb(wa_buf* b, int64_t v) { + int more = 1; + while (more) { + uint8_t byte = v & 0x7f; + v >>= 7; /* arithmetic shift */ + if ((v == 0 && !(byte & 0x40)) || (v == -1 && (byte & 0x40))) + more = 0; + else + byte |= 0x80; + wa_buf_u8(b, byte); + } +} + +/* ------------------------------------------------------------------ */ +/* Function builder */ +/* ------------------------------------------------------------------ */ + +typedef struct { + wa_buf params; /* raw valtype bytes */ + int n_params; + wa_buf results; /* raw valtype bytes */ + int n_results; + wa_buf locals; /* encoded local runs: uleb(count) valtype, ... */ + int n_local_runs; + wa_buf code; /* instruction bytes incl. trailing `end` */ +} wa_func; + +static inline void wa_func_init(wa_func* f) { memset(f, 0, sizeof(*f)); } + +static inline void wa_func_free(wa_func* f) { + free(f->params.data); + free(f->results.data); + free(f->locals.data); + free(f->code.data); + memset(f, 0, sizeof(*f)); +} + +static inline void wa_param(wa_func* f, wa_valtype t) { + wa_buf_u8(&f->params, (uint8_t)t); + f->n_params++; +} + +static inline void wa_result(wa_func* f, wa_valtype t) { + wa_buf_u8(&f->results, (uint8_t)t); + f->n_results++; +} + +/* Add `count` locals of type `t`. Locals are indexed after the parameters. */ +static inline void wa_local(wa_func* f, wa_valtype t, int count) { + wa_buf_uleb(&f->locals, (uint64_t)count); + wa_buf_u8(&f->locals, (uint8_t)t); + f->n_local_runs++; +} + +/* Raw opcode / immediate emitters. */ +static inline void wa_op(wa_func* f, uint8_t op) { wa_buf_u8(&f->code, op); } +static inline void wa_uleb(wa_func* f, uint64_t v) { wa_buf_uleb(&f->code, v); } +static inline void wa_sleb(wa_func* f, int64_t v) { wa_buf_sleb(&f->code, v); } + +/* Convenience emitters for the opcodes used in the example. */ +static inline void wa_local_get(wa_func* f, uint32_t i) { + wa_op(f, 0x20); + wa_uleb(f, i); +} +static inline void wa_local_set(wa_func* f, uint32_t i) { + wa_op(f, 0x21); + wa_uleb(f, i); +} +static inline void wa_local_tee(wa_func* f, uint32_t i) { + wa_op(f, 0x22); + wa_uleb(f, i); +} +static inline void wa_global_get(wa_func* f, uint32_t i) { + wa_op(f, 0x23); + wa_uleb(f, i); +} +static inline void wa_global_set(wa_func* f, uint32_t i) { + wa_op(f, 0x24); + wa_uleb(f, i); +} +static inline void wa_i32_const(wa_func* f, int32_t v) { + wa_op(f, 0x41); + wa_sleb(f, v); +} +static inline void wa_i32_add(wa_func* f) { wa_op(f, 0x6a); } +static inline void wa_i32_sub(wa_func* f) { wa_op(f, 0x6b); } +static inline void wa_i32_mul(wa_func* f) { wa_op(f, 0x6c); } +static inline void wa_i32_load(wa_func* f, uint32_t align, uint32_t off) { + wa_op(f, 0x28); + wa_uleb(f, align); + wa_uleb(f, off); +} +static inline void wa_i32_store(wa_func* f, uint32_t align, uint32_t off) { + wa_op(f, 0x36); + wa_uleb(f, align); + wa_uleb(f, off); +} +static inline void wa_call(wa_func* f, uint32_t fn) { + wa_op(f, 0x10); + wa_uleb(f, fn); +} +static inline void wa_call_indirect(wa_func* f, uint32_t type, uint32_t table) { + wa_op(f, 0x11); + wa_uleb(f, type); + wa_uleb(f, table); +} +static inline void wa_drop(wa_func* f) { wa_op(f, 0x1a); } +static inline void wa_end(wa_func* f) { wa_op(f, 0x0b); } + +/* + * Serialize a finished function into a blob. The returned buffer is malloc'd; + * the caller owns it. + */ +static inline void wa_func_finish(wa_func* f, uint8_t** out_bytes, + size_t* out_len) { + wa_buf blob = {0}; + wa_buf_u8(&blob, 0x60); + wa_buf_uleb(&blob, (uint64_t)f->n_params); + wa_buf_bytes(&blob, f->params.data, f->params.len); + wa_buf_uleb(&blob, (uint64_t)f->n_results); + wa_buf_bytes(&blob, f->results.data, f->results.len); + wa_buf_uleb(&blob, (uint64_t)f->n_local_runs); + wa_buf_bytes(&blob, f->locals.data, f->locals.len); + wa_buf_bytes(&blob, f->code.data, f->code.len); + *out_bytes = blob.data; + *out_len = blob.len; +} + +/* + * Convenience wrapper: finish `n` functions and hand them to the runner in one + * call. `out` must have room for `n` funcptrs. + */ +static inline wa_err wa_add_funcs(wa_func* funcs, int n, wa_funcptr* out) { + uint8_t** bytecode = (uint8_t**)malloc((size_t)n * sizeof(uint8_t*)); + size_t* lens = (size_t*)malloc((size_t)n * sizeof(size_t)); + for (int i = 0; i < n; i++) wa_func_finish(&funcs[i], &bytecode[i], &lens[i]); + wa_err e = wasm_add_funcs(bytecode, lens, n, out); + for (int i = 0; i < n; i++) free(bytecode[i]); + free(bytecode); + free(lens); + return e; +} + +static inline wa_err wa_add_funcs2(wa_func* funcs, int n, + const wa_funcptr* extern_funcs, int nextern, + wa_funcptr* out) { + uint8_t** bytecode = (uint8_t**)malloc((size_t)n * sizeof(uint8_t*)); + size_t* lens = (size_t*)malloc((size_t)n * sizeof(size_t)); + for (int i = 0; i < n; i++) wa_func_finish(&funcs[i], &bytecode[i], &lens[i]); + wa_err e = wasm_add_funcs2(bytecode, lens, n, extern_funcs, nextern, out); + for (int i = 0; i < n; i++) free(bytecode[i]); + free(bytecode); + free(lens); + return e; +} + +#endif /* WASM_BUILD_H */ diff --git a/js/src/night/wasm-jit-runner/src/addfuncs.rs b/js/src/night/wasm-jit-runner/src/addfuncs.rs new file mode 100644 index 0000000000000..c2a4b1d11081f --- /dev/null +++ b/js/src/night/wasm-jit-runner/src/addfuncs.rs @@ -0,0 +1,532 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Implementation of the `wasm_add_funcs` host import. +//! +//! The guest calls: +//! +//! ```c +//! err_t wasm_add_funcs(uint8_t** bytecode, size_t* lens, int nfuncs, funcptr_t* out); +//! ``` +//! +//! Each `bytecode[i]` (of length `lens[i]`) is a self-describing "function blob" +//! (see [`parse_blob`]). The host assembles all `nfuncs` blobs into a single new +//! core-wasm module in which: +//! +//! * function indices `0..n_extern` are the imported extern (helper) +//! functions, and function index `n_extern + i` is the i-th supplied blob, +//! so the new functions can `call` each other and the helpers by index; +//! * the existing guest module's memories, tables and globals are imported, at +//! the same indices they have in the guest (so new code can reference them +//! directly), but the guest's *functions* are deliberately not visible. +//! +//! The new module is instantiated into the same store. Each new function is then +//! appended to table 0 (the guest's funcptr / `__indirect_function_table`) and +//! the resulting table indices ("funcptrs") are written back to `out`. + +use crate::modedit::{GLOBAL_PREFIX, MEM_PREFIX, TABLE_PREFIX}; +use crate::{Host, WtResultExt}; +use anyhow::{bail, Context, Result}; +use wasmtime::{Caller, Engine, Extern, Func, Global, Instance, Memory, Module, Ref, Table}; + +/// A parsed function blob. +struct ParsedFunc { + params: Vec, + results: Vec, + /// The function body: a `vec(locals)` followed by the instruction `expr` + /// (terminated by the `end` opcode), exactly as it appears in a wasm code + /// section entry (but without the leading byte-size prefix). + body: Vec, +} + +/// Convert a wasmparser valtype to a wasm-encoder valtype. +fn enc_valtype(t: &wasmparser::ValType) -> Result { + use wasm_encoder::reencode::{Reencode, RoundtripReencoder}; + RoundtripReencoder + .val_type(*t) + .map_err(|e| anyhow::anyhow!("valtype: {e:?}")) +} + +/// Parse a single function blob. Format: +/// +/// ```text +/// 0x60 ; functype tag +/// vec(valtype) params ; standard wasm functype encoding +/// vec(valtype) results +/// ; the rest of the blob: vec(locals) ++ expr +/// ``` +/// +/// The functype is decoded with wasmparser; the remaining bytes are the wasm +/// code body, which we keep verbatim (wasmtime validates it on compile). +fn parse_blob(bytes: &[u8]) -> Result { + let mut reader = wasmparser::BinaryReader::new(bytes, 0); + let tag = reader.read_u8().context("empty function blob")?; + if tag != 0x60 { + bail!("function blob must start with functype tag 0x60, got 0x{tag:02x}"); + } + let func_ty: wasmparser::FuncType = reader.read().context("decoding functype in blob")?; + let params = func_ty + .params() + .iter() + .map(enc_valtype) + .collect::>>()?; + let results = func_ty + .results() + .iter() + .map(enc_valtype) + .collect::>>()?; + + let body = bytes + .get(reader.current_position()..) + .filter(|b| !b.is_empty()) + .context("function blob has empty code body")? + .to_vec(); + Ok(ParsedFunc { + params, + results, + body, + }) +} + +// --------------------------------------------------------------------------- +// wasmtime type -> wasm-encoder type conversions +// --------------------------------------------------------------------------- + +fn ref_to_enc(r: &wasmtime::RefType) -> Result { + use wasm_encoder::{AbstractHeapType, HeapType}; + use wasmtime::HeapType as H; + let ty = match r.heap_type() { + H::Func | H::ConcreteFunc(_) | H::NoFunc => AbstractHeapType::Func, + H::Extern | H::NoExtern => AbstractHeapType::Extern, + other => bail!("unsupported heap type in import: {other:?}"), + }; + Ok(wasm_encoder::RefType { + nullable: r.is_nullable(), + heap_type: HeapType::Abstract { shared: false, ty }, + }) +} + +fn val_to_enc(v: &wasmtime::ValType) -> Result { + use wasm_encoder::ValType as E; + use wasmtime::ValType as W; + Ok(match v { + W::I32 => E::I32, + W::I64 => E::I64, + W::F32 => E::F32, + W::F64 => E::F64, + W::V128 => E::V128, + W::Ref(r) => E::Ref(ref_to_enc(r)?), + }) +} + +/// Types of the imports the new module needs, gathered from the live guest +/// instance, in import order (memories, then tables, then globals). +struct ImportTypes { + mems: Vec, + tables: Vec, + globals: Vec, +} + +/// Assemble the new module's bytes from the parsed functions, extern-function +/// types (imported functions occupying indices `0..externs.len()`, so blob +/// `call` immediates can reference them and blob i is function index +/// `externs.len() + i`), and item import types. +fn build_module( + funcs: &[ParsedFunc], + externs: &[(Vec, Vec)], + imports: &ImportTypes, +) -> Vec { + use wasm_encoder::{ + CodeSection, EntityType, ExportKind, ExportSection, FunctionSection, ImportSection, Module, + TypeSection, + }; + + let n_extern = externs.len() as u32; + let mut module = Module::new(); + + // Type section: extern-function types first, then one type per supplied + // function, in order. + let mut types = TypeSection::new(); + for (params, results) in externs { + types + .ty() + .function(params.iter().copied(), results.iter().copied()); + } + for f in funcs { + types + .ty() + .function(f.params.iter().copied(), f.results.iter().copied()); + } + module.section(&types); + + // Import section: functions first (they occupy the low function indices), + // then memories, tables and globals matching the guest's index spaces. + // Field names are arbitrary (imports resolve positionally). + let mut import_sec = ImportSection::new(); + let mut field = 0u32; + for i in 0..n_extern { + import_sec.import("e", &format!("i{field}"), EntityType::Function(i)); + field += 1; + } + for mt in &imports.mems { + import_sec.import("e", &format!("i{field}"), EntityType::Memory(*mt)); + field += 1; + } + for tt in &imports.tables { + import_sec.import("e", &format!("i{field}"), EntityType::Table(*tt)); + field += 1; + } + for gt in &imports.globals { + import_sec.import("e", &format!("i{field}"), EntityType::Global(*gt)); + field += 1; + } + module.section(&import_sec); + + // Function section: blob i uses type n_extern + i. + let mut func_sec = FunctionSection::new(); + for i in 0..funcs.len() as u32 { + func_sec.function(n_extern + i); + } + module.section(&func_sec); + + // Export section: export each blob function so the host can grab a handle. + let mut export_sec = ExportSection::new(); + for i in 0..funcs.len() as u32 { + export_sec.export(&format!("f{i}"), ExportKind::Func, n_extern + i); + } + module.section(&export_sec); + + // Code section. We already have raw bodies, so build the section payload by + // hand and splice it in as a raw section. + let mut code = CodeSection::new(); + for f in funcs { + // `CodeSection::raw` length-prefixes its argument, turning `locals ++ + // expr` into a complete (size-prefixed) code-section entry. + code.raw(&f.body); + } + module.section(&code); + + module.finish() +} + +// --------------------------------------------------------------------------- +// Host function +// --------------------------------------------------------------------------- + +fn read_u32(data: &[u8], addr: u32) -> Result { + let a = addr as usize; + let slice = data.get(a..a + 4).context("guest pointer out of bounds")?; + Ok(u32::from_le_bytes(slice.try_into().unwrap())) +} + +/// Address of element `i` of a u32 array at `base`. Plain `base + i * 4` +/// wraps in the guest's 32-bit address space, and a wrapped address lands +/// back in bounds, so it would pass every later check while naming the +/// wrong memory. +fn elem_addr(base: u32, i: u32) -> Result { + i.checked_mul(4) + .and_then(|off| base.checked_add(off)) + .context("guest array address overflows") +} + +/// Core implementation; returns `Ok(())` on success. Any error is reported to +/// the guest as a non-zero error code (and logged to stderr). +fn add_funcs_impl( + caller: &mut Caller<'_, Host>, + bytecode_arr: u32, + lens_arr: u32, + nfuncs: i32, + extern_arr: u32, + nextern: i32, + out_ptr: u32, +) -> Result<()> { + if nfuncs < 0 || nextern < 0 { + bail!("negative nfuncs/nextern"); + } + let n = nfuncs as usize; + let n_extern = nextern as usize; + if n == 0 { + return Ok(()); + } + + let layout = caller.data().layout; + + // The guest's main memory (WASI exports it as "memory"). + let memory: Memory = caller + .get_export("memory") + .and_then(Extern::into_memory) + .context("guest has no exported `memory`")?; + + // Read all blob pointers/lengths, the blob bytes, and the extern-function + // table indices out of guest memory into owned buffers, so we can drop the + // immutable borrow before mutating store. + let (blobs, extern_indices): (Vec>, Vec) = { + let data = memory.data(&caller); + // Both counts index u32 arrays in guest memory, so a count past + // that many words in the whole memory cannot be honest; refuse it + // before it sizes an allocation. + let max_entries = data.len() / 4; + if n > max_entries || n_extern > max_entries { + bail!("nfuncs/nextern exceed guest memory ({n}, {n_extern})"); + } + let mut blobs = Vec::with_capacity(n); + for i in 0..n as u32 { + let ptr = read_u32(data, elem_addr(bytecode_arr, i)?)?; + let len = read_u32(data, elem_addr(lens_arr, i)?)?; + let start = ptr as usize; + let end = start + .checked_add(len as usize) + .filter(|&e| e <= data.len()) + .context("blob bytes out of bounds")?; + blobs.push(data[start..end].to_vec()); + } + let mut extern_indices = Vec::with_capacity(n_extern); + for i in 0..n_extern as u32 { + extern_indices.push(read_u32(data, elem_addr(extern_arr, i)?)?); + } + (blobs, extern_indices) + }; + + let funcs: Vec = blobs + .iter() + .enumerate() + .map(|(i, b)| parse_blob(b).with_context(|| format!("parsing function blob {i}"))) + .collect::>()?; + + // Gather handles to the guest's memories, tables and globals (added as + // synthetic exports during module editing), in import order. + let mut mem_externs: Vec = Vec::new(); + for i in 0..layout.n_mem { + let m = caller + .get_export(&format!("{MEM_PREFIX}{i}")) + .and_then(Extern::into_memory) + .with_context(|| format!("missing export {MEM_PREFIX}{i}"))?; + mem_externs.push(m); + } + let mut table_externs: Vec = Vec::new(); + for i in 0..layout.n_table { + let t = caller + .get_export(&format!("{TABLE_PREFIX}{i}")) + .and_then(Extern::into_table) + .with_context(|| format!("missing export {TABLE_PREFIX}{i}"))?; + table_externs.push(t); + } + let mut global_externs: Vec = Vec::new(); + for i in 0..layout.n_global { + let g = caller + .get_export(&format!("{GLOBAL_PREFIX}{i}")) + .and_then(Extern::into_global) + .with_context(|| format!("missing export {GLOBAL_PREFIX}{i}"))?; + global_externs.push(g); + } + + // Resolve extern functions from live table-0 entries: each guest-supplied + // index must hold a funcref; its type becomes the corresponding function + // import's type (so a type mismatch fails instantiation loudly). + let mut extern_funcs: Vec = Vec::with_capacity(n_extern); + let mut extern_types: Vec<(Vec, Vec)> = + Vec::with_capacity(n_extern); + if n_extern > 0 { + let t0 = *table_externs + .first() + .context("guest has no table 0 for extern functions")?; + for (i, &idx) in extern_indices.iter().enumerate() { + let elem = t0 + .get(&mut *caller, idx as u64) + .with_context(|| format!("extern {i}: table index {idx} out of bounds"))?; + let f = match elem { + Ref::Func(Some(f)) => f, + _ => bail!("extern {i}: table index {idx} does not hold a function"), + }; + let ty = f.ty(&*caller); + let params = ty + .params() + .map(|p| val_to_enc(&p)) + .collect::>>()?; + let results = ty + .results() + .map(|r| val_to_enc(&r)) + .collect::>>()?; + extern_funcs.push(f); + extern_types.push((params, results)); + } + } + + // Derive the import types from the live items. + let import_types = ImportTypes { + mems: mem_externs + .iter() + .map(|m| { + let t = m.ty(&caller); + // Relax the limits to a supertype so matching always succeeds; + // preserve the 64-bit/shared identity bits. + wasm_encoder::MemoryType { + minimum: 0, + maximum: None, + memory64: t.is_64(), + shared: t.is_shared(), + page_size_log2: None, + } + }) + .collect(), + tables: table_externs + .iter() + .map(|t| { + let ty = t.ty(&caller); + Ok(wasm_encoder::TableType { + element_type: ref_to_enc(ty.element())?, + table64: false, + minimum: 0, + maximum: None, + shared: false, + }) + }) + .collect::>()?, + globals: global_externs + .iter() + .map(|g| { + let ty = g.ty(&caller); + Ok(wasm_encoder::GlobalType { + val_type: val_to_enc(ty.content())?, + mutable: matches!(ty.mutability(), wasmtime::Mutability::Var), + shared: false, + }) + }) + .collect::>()?, + }; + + // Build and compile the new module. + let wasm = build_module(&funcs, &extern_types, &import_types); + let engine: Engine = caller.engine().clone(); + let module = Module::new(&engine, &wasm) + .anyhow() + .context("compiling dynamically-added module")?; + + // Imports, in the same order the module declares them. + let mut imports: Vec = Vec::new(); + imports.extend(extern_funcs.iter().map(|f| Extern::Func(*f))); + imports.extend(mem_externs.iter().map(|m| Extern::Memory(*m))); + imports.extend(table_externs.iter().map(|t| Extern::Table(*t))); + imports.extend(global_externs.iter().map(|g| Extern::Global(*g))); + + let instance: Instance = Instance::new(&mut *caller, &module, &imports) + .anyhow() + .context("instantiating dynamically-added module")?; + + // Collect the new functions. + let mut new_funcs: Vec = Vec::with_capacity(n); + for i in 0..n as u32 { + let f = instance + .get_func(&mut *caller, &format!("f{i}")) + .with_context(|| format!("new module missing export f{i}"))?; + new_funcs.push(f); + } + + // Append them to table 0 (the funcptr table) and record their indices. + let table0 = *table_externs + .first() + .context("guest has no table 0 to hold funcptrs")?; + let base = table0 + .grow(&mut *caller, n as u64, Ref::Func(None)) + .anyhow() + .context("growing funcptr table (build guest with -Wl,--growable-table)")?; + for (i, f) in new_funcs.iter().enumerate() { + table0 + .set(&mut *caller, base + i as u64, Ref::Func(Some(*f))) + .anyhow() + .context("setting funcptr table entry")?; + } + + // Write the resulting funcptrs back to the guest's `out` array. + { + let data = memory.data_mut(&mut *caller); + for i in 0..n { + let funcptr = (base + i as u64) as u32; + let addr = elem_addr(out_ptr, i as u32)? as usize; + let slot = data + .get_mut(addr..addr + 4) + .context("out pointer out of bounds")?; + slot.copy_from_slice(&funcptr.to_le_bytes()); + } + } + + Ok(()) +} + +/// The current size of table 0 (the funcptr table). Because added functions +/// are appended contiguously, a guest that queries this before calling +/// `wasm_add_funcs*` can predict the returned indices: blob i lands at +/// `size + i`. This contiguity is an API guarantee. +fn table_size_impl(caller: &mut Caller<'_, Host>) -> Result { + let t0 = caller + .get_export(&format!("{TABLE_PREFIX}0")) + .and_then(Extern::into_table) + .context("guest has no table 0")?; + Ok(t0.size(&*caller) as u32) +} + +/// Register `env.wasm_add_funcs`, `env.wasm_add_funcs2` and +/// `env.wasm_table_size` on the linker. +pub fn add_to_linker(linker: &mut wasmtime::Linker) -> Result<()> { + linker.func_wrap( + "env", + "wasm_add_funcs", + |mut caller: Caller<'_, Host>, + bytecode_arr: u32, + lens_arr: u32, + nfuncs: i32, + out_ptr: u32| + -> i32 { + match add_funcs_impl(&mut caller, bytecode_arr, lens_arr, nfuncs, 0, 0, out_ptr) { + Ok(()) => 0, + Err(e) => { + eprintln!("[wasm-jit-runner] wasm_add_funcs failed: {e:?}"); + 1 + } + } + }, + )?; + linker.func_wrap( + "env", + "wasm_add_funcs2", + |mut caller: Caller<'_, Host>, + bytecode_arr: u32, + lens_arr: u32, + nfuncs: i32, + extern_arr: u32, + nextern: i32, + out_ptr: u32| + -> i32 { + match add_funcs_impl( + &mut caller, + bytecode_arr, + lens_arr, + nfuncs, + extern_arr, + nextern, + out_ptr, + ) { + Ok(()) => 0, + Err(e) => { + eprintln!("[wasm-jit-runner] wasm_add_funcs2 failed: {e:?}"); + 1 + } + } + }, + )?; + linker.func_wrap( + "env", + "wasm_table_size", + |mut caller: Caller<'_, Host>| -> i32 { + match table_size_impl(&mut caller) { + Ok(sz) => sz as i32, + Err(e) => { + eprintln!("[wasm-jit-runner] wasm_table_size failed: {e:?}"); + -1 + } + } + }, + )?; + Ok(()) +} diff --git a/js/src/night/wasm-jit-runner/src/cache.rs b/js/src/night/wasm-jit-runner/src/cache.rs new file mode 100644 index 0000000000000..3c7e0501ca3e3 --- /dev/null +++ b/js/src/night/wasm-jit-runner/src/cache.rs @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Content-addressed compiled-module (.cwasm) cache. +//! +//! Keyed by sha256 of the (edited) module bytes plus the wasmtime version, so +//! a rebuilt guest or a runner upgrade never sees a stale entry. The cache +//! directory is trusted: `Module::deserialize_file` runs no validation on the +//! precompiled code. + +use anyhow::{Context, Result}; +use sha2::{Digest, Sha256}; +use wasmtime::{Engine, Module}; + +use crate::WtResultExt; + +pub fn load_module(engine: &Engine, bytes: &[u8], cache_dir: Option<&str>) -> Result { + let Some(dir) = cache_dir else { + return Module::new(engine, bytes).anyhow(); + }; + + let mut hasher = Sha256::new(); + hasher.update(env!("CARGO_PKG_VERSION").as_bytes()); + // Covers the wasmtime version, target and engine config, so a runner or + // config change never hits a stale entry. + { + use std::hash::{Hash, Hasher}; + let mut h = std::hash::DefaultHasher::new(); + engine.precompile_compatibility_hash().hash(&mut h); + hasher.update(h.finish().to_le_bytes()); + } + hasher.update(bytes); + let key = hex(&hasher.finalize()); + let path = std::path::Path::new(dir).join(format!("{key}.cwasm")); + + if path.exists() { + // SAFETY: the cache entry was serialized by this same runner version + // from these same module bytes; the directory is trusted. + match unsafe { Module::deserialize_file(engine, &path) } { + Ok(m) => return Ok(m), + Err(e) => { + eprintln!("[wasm-jit-runner] ignoring bad cache entry {path:?}: {e}"); + } + } + } + + let module = Module::new(engine, bytes).anyhow()?; + std::fs::create_dir_all(dir).with_context(|| format!("creating cache dir {dir}"))?; + let serialized = module.serialize().anyhow()?; + // Write-then-rename so concurrent runners never observe a partial entry. + let tmp = path.with_extension(format!("tmp.{}", std::process::id())); + std::fs::write(&tmp, &serialized).with_context(|| format!("writing {tmp:?}"))?; + std::fs::rename(&tmp, &path).with_context(|| format!("renaming into {path:?}"))?; + Ok(module) +} + +fn hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + s.push_str(&format!("{b:02x}")); + } + s +} diff --git a/js/src/night/wasm-jit-runner/src/main.rs b/js/src/night/wasm-jit-runner/src/main.rs new file mode 100644 index 0000000000000..d13f65f7966e8 --- /dev/null +++ b/js/src/night/wasm-jit-runner/src/main.rs @@ -0,0 +1,195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! wasm-jit-runner: a small WASI (preview 1) CLI runner built on the `wasmtime` +//! crate, with one extra trick: it exposes a host import, `env.wasm_add_funcs`, +//! that lets the running guest add *new wasm functions to itself on the fly* and +//! call them — without a round-trip back out to the runner. This is handy for +//! testing a wasm-targeting compiler "in situ". +//! +//! Usage: +//! +//! ```text +//! wasm-jit-runner [guest args...] +//! ``` + +mod addfuncs; +mod cache; +mod modedit; + +use anyhow::{Context, Result}; +use modedit::ModuleLayout; +use wasmtime::{Config, Engine, Linker, Store}; +use wasmtime_wasi::p1::WasiP1Ctx; +use wasmtime_wasi::WasiCtxBuilder; + +/// `wasmtime` 45 uses its own `Error`/`Result` types rather than `anyhow`. +/// This adapter converts a `wasmtime::Result` into an `anyhow::Result` so the +/// two compose with `?` and `.context(..)`. +pub trait WtResultExt { + fn anyhow(self) -> anyhow::Result; +} +impl WtResultExt for wasmtime::Result { + fn anyhow(self) -> anyhow::Result { + self.map_err(anyhow::Error::from) + } +} + +/// Per-store host state: the WASI context plus the guest module's layout (how +/// many memories/tables/globals it has), needed by the `wasm_add_funcs` import. +pub struct Host { + wasi: WasiP1Ctx, + layout: ModuleLayout, +} + +struct Options { + module_path: String, + guest_args: Vec, + /// Host dirs to preopen, as (host, guest) path pairs. + dirs: Vec<(String, String)>, + cache_dir: Option, +} + +fn usage(exe: &str) -> ! { + eprintln!( + "usage: {exe} [--dir HOST[::GUEST]]... [--cache-dir DIR] [--] \ + [guest args...]" + ); + std::process::exit(2); +} + +fn parse_args() -> Options { + let mut args = std::env::args(); + let exe = args.next().unwrap_or_else(|| "wasm-jit-runner".into()); + let mut dirs: Vec<(String, String)> = Vec::new(); + let mut cache_dir: Option = None; + let mut module_path: Option = None; + while let Some(a) = args.next() { + match a.as_str() { + "--dir" => { + let v = args.next().unwrap_or_else(|| usage(&exe)); + let (host, guest) = match v.split_once("::") { + Some((h, g)) => (h.to_string(), g.to_string()), + None => (v.clone(), v.clone()), + }; + dirs.push((host, guest)); + } + "--cache-dir" => { + cache_dir = Some(args.next().unwrap_or_else(|| usage(&exe))); + } + "--" => { + module_path = args.next(); + break; + } + _ => { + module_path = Some(a); + break; + } + } + } + let Some(module_path) = module_path else { + usage(&exe); + }; + if dirs.is_empty() { + // Default: give the guest the whole host filesystem, so absolute + // paths (e.g. test files and -f includes) resolve. + dirs.push(("/".to_string(), "/".to_string())); + } + let mut guest_args: Vec = args.collect(); + // Support `runner -- `: a solo `--` right after + // the module separates runner args from guest args; drop it so the guest + // (e.g. the JS shell, where `--` ends option parsing) still sees its + // options. + if guest_args.first().is_some_and(|a| a == "--") { + guest_args.remove(0); + } + Options { + module_path, + guest_args, + dirs, + cache_dir, + } +} + +/// Native wasm stack budget: deep guest recursion (e.g. JS self-recursion in +/// AOT-compiled bodies) must hit the guest's own catchable limits before the +/// host stack runs out. The runner work runs on a thread whose stack exceeds +/// this by a margin. +const MAX_WASM_STACK: usize = 256 * 1024 * 1024; + +fn main() -> Result<()> { + std::thread::Builder::new() + .stack_size(MAX_WASM_STACK + 32 * 1024 * 1024) + .spawn(run) + .context("spawning runner thread")? + .join() + .map_err(|_| anyhow::anyhow!("runner thread panicked"))? +} + +fn run() -> Result<()> { + let opts = parse_args(); + let module_path = &opts.module_path; + + let mut config = Config::new(); + config.max_wasm_stack(MAX_WASM_STACK); + config.async_stack_size(MAX_WASM_STACK + 16 * 1024 * 1024); + let engine = Engine::new(&config)?; + + // Load the guest module and rewrite it so all memories/tables/globals are + // exported (we need handles to them at runtime). + let raw = + std::fs::read(module_path).with_context(|| format!("reading module {module_path}"))?; + let (edited, layout) = modedit::add_item_exports(&raw) + .with_context(|| format!("preparing module {module_path}"))?; + let module = cache::load_module(&engine, &edited, opts.cache_dir.as_deref()) + .context("compiling guest module")?; + + // Set up the linker: WASI preview1 plus our `wasm_add_funcs` import. + let mut linker: Linker = Linker::new(&engine); + wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |h: &mut Host| &mut h.wasi) + .anyhow() + .context("adding WASI to linker")?; + addfuncs::add_to_linker(&mut linker).context("adding wasm_add_funcs to linker")?; + + // Build the WASI context: inherit stdio/env, preopen dirs, pass argv. + let mut builder = WasiCtxBuilder::new(); + builder.inherit_stdio().inherit_env(); + for (host, guest) in &opts.dirs { + builder + .preopened_dir( + host, + guest, + wasmtime_wasi::DirPerms::all(), + wasmtime_wasi::FilePerms::all(), + ) + .anyhow() + .with_context(|| format!("preopening {host} as {guest}"))?; + } + builder.arg(module_path); + for a in &opts.guest_args { + builder.arg(a); + } + let wasi = builder.build_p1(); + + let mut store = Store::new(&engine, Host { wasi, layout }); + + let instance = linker + .instantiate(&mut store, &module) + .anyhow() + .context("instantiating guest module")?; + let start = instance + .get_typed_func::<(), ()>(&mut store, "_start") + .anyhow() + .context("guest module has no `_start` (is it a WASI command?)")?; + + match start.call(&mut store, ()) { + Ok(()) => Ok(()), + Err(e) => { + if let Some(exit) = e.downcast_ref::() { + std::process::exit(exit.0); + } + Err(anyhow::Error::from(e)).context("guest trapped") + } + } +} diff --git a/js/src/night/wasm-jit-runner/src/modedit.rs b/js/src/night/wasm-jit-runner/src/modedit.rs new file mode 100644 index 0000000000000..5ae6fd4da4e5b --- /dev/null +++ b/js/src/night/wasm-jit-runner/src/modedit.rs @@ -0,0 +1,210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Stream-editing of the guest module so that all of its memories, tables and +//! globals are exported under synthetic names. The runner needs handles to +//! these items at runtime so that dynamically-added functions can import them +//! (and so we can append new entries to the funcptr table). + +use std::ops::Range; + +use anyhow::Result; +use wasmparser::{Parser, Payload, TypeRef}; + +/// Synthetic export-name prefixes. Chosen to be very unlikely to collide with +/// names a real toolchain would emit. +pub const MEM_PREFIX: &str = "__wjr_mem"; +pub const TABLE_PREFIX: &str = "__wjr_table"; +pub const GLOBAL_PREFIX: &str = "__wjr_global"; + +/// Number of memories / tables / globals in the (edited) guest module. Each is +/// exported as `` for index in `0..count`. +#[derive(Clone, Copy, Debug)] +pub struct ModuleLayout { + pub n_mem: u32, + pub n_table: u32, + pub n_global: u32, +} + +fn map_kind(k: wasmparser::ExternalKind) -> wasm_encoder::ExportKind { + use wasm_encoder::ExportKind as X; + use wasmparser::ExternalKind as E; + match k { + E::Func => X::Func, + E::Table => X::Table, + E::Memory => X::Memory, + E::Global => X::Global, + E::Tag => X::Tag, + // `FuncExact` (typed function references) is still a function export as + // far as the export *kind* byte is concerned. + E::FuncExact => X::Func, + } +} + +/// Build the export section: any pre-existing exports, followed by a synthetic +/// export for every memory, table and global. +fn build_export_section( + existing: &[(String, wasmparser::ExternalKind, u32)], + layout: ModuleLayout, +) -> wasm_encoder::ExportSection { + use wasm_encoder::ExportKind; + let mut sec = wasm_encoder::ExportSection::new(); + for (name, kind, index) in existing { + sec.export(name, map_kind(*kind), *index); + } + for i in 0..layout.n_mem { + sec.export(&format!("{MEM_PREFIX}{i}"), ExportKind::Memory, i); + } + for i in 0..layout.n_table { + sec.export(&format!("{TABLE_PREFIX}{i}"), ExportKind::Table, i); + } + for i in 0..layout.n_global { + sec.export(&format!("{GLOBAL_PREFIX}{i}"), ExportKind::Global, i); + } + sec +} + +/// For a section payload we copy through verbatim, return its section id and the +/// byte range of its contents (which `range()` already excludes the id/size +/// header from). Returns `None` for payloads we handle specially, for the +/// per-entry code payloads (covered by `CodeSectionStart`'s range), and for +/// non-section payloads such as the header and end markers. +fn passthrough_section(payload: &Payload) -> Option<(u8, Range)> { + Some(match payload { + Payload::CustomSection(r) => (0, r.range()), + Payload::TypeSection(r) => (1, r.range()), + Payload::FunctionSection(r) => (3, r.range()), + Payload::StartSection { range, .. } => (8, range.clone()), + Payload::ElementSection(r) => (9, r.range()), + Payload::CodeSectionStart { range, .. } => (10, range.clone()), + Payload::DataSection(r) => (11, r.range()), + Payload::DataCountSection { range, .. } => (12, range.clone()), + Payload::TagSection(r) => (13, r.range()), + _ => return None, + }) +} + +/// Rewrite `wasm` so that every memory, table and global is exported under a +/// synthetic name (in addition to any existing exports), and so that every +/// table type has its maximum stripped (making the funcptr table growable, so +/// the guest needs no `-Wl,--growable-table`). Returns the new module bytes plus +/// the layout describing how many of each item exist. +pub fn add_item_exports(wasm: &[u8]) -> Result<(Vec, ModuleLayout)> { + use wasm_encoder::reencode::{Reencode, RoundtripReencoder}; + use wasm_encoder::{EntityType, RawSection}; + + let mut module = wasm_encoder::Module::new(); + let (mut n_mem, mut n_table, mut n_global) = (0u32, 0u32, 0u32); + let mut exports_done = false; + let mut reenc = RoundtripReencoder; + let reencode_err = + |e: wasm_encoder::reencode::Error| anyhow::anyhow!("re-encoding module: {e:?}"); + + // Single pass over wasmparser's per-section payloads, emitting each section + // (in order) into the output. Most sections are copied verbatim via their + // content range; the import, table and export sections are rebuilt. Counts + // of memories/tables/globals are complete by the time we reach the export + // section (or, for modules with none, the first section that follows it). + for payload in Parser::new(0).parse_all(wasm) { + let payload = payload?; + let layout = ModuleLayout { + n_mem, + n_table, + n_global, + }; + + // If the module has no export section, insert one just before the first + // section that must follow exports (section id >= 8). + if !exports_done { + if let Some((id, _)) = passthrough_section(&payload) { + if id >= 8 { + module.section(&build_export_section(&[], layout)); + exports_done = true; + } + } + } + + match payload { + Payload::ImportSection(reader) => { + let mut isec = wasm_encoder::ImportSection::new(); + for imp in reader.into_imports() { + let imp = imp?; + match imp.ty { + TypeRef::Memory(_) => n_mem += 1, + TypeRef::Table(_) => n_table += 1, + TypeRef::Global(_) => n_global += 1, + _ => {} + } + // Strip the maximum off imported tables so they stay growable. + let mut ety = reenc.entity_type(imp.ty).map_err(reencode_err)?; + if let EntityType::Table(t) = &mut ety { + t.maximum = None; + } + isec.import(imp.module, imp.name, ety); + } + module.section(&isec); + } + Payload::TableSection(reader) => { + n_table += reader.count(); + let mut tsec = wasm_encoder::TableSection::new(); + for table in reader { + let table = table?; + let mut ty = reenc.table_type(table.ty).map_err(reencode_err)?; + ty.maximum = None; // make the funcptr table growable + match table.init { + wasmparser::TableInit::RefNull => { + tsec.table(ty); + } + wasmparser::TableInit::Expr(e) => { + tsec.table_with_init(ty, &reenc.const_expr(e).map_err(reencode_err)?); + } + } + } + module.section(&tsec); + } + Payload::MemorySection(reader) => { + n_mem += reader.count(); + module.section(&RawSection { + id: 5, + data: &wasm[reader.range()], + }); + } + Payload::GlobalSection(reader) => { + n_global += reader.count(); + module.section(&RawSection { + id: 6, + data: &wasm[reader.range()], + }); + } + Payload::ExportSection(reader) => { + let mut existing = Vec::new(); + for e in reader { + let e = e?; + existing.push((e.name.to_string(), e.kind, e.index)); + } + module.section(&build_export_section(&existing, layout)); + exports_done = true; + } + other => { + if let Some((id, range)) = passthrough_section(&other) { + module.section(&RawSection { + id, + data: &wasm[range], + }); + } + } + } + } + + let layout = ModuleLayout { + n_mem, + n_table, + n_global, + }; + if !exports_done { + module.section(&build_export_section(&[], layout)); + } + + Ok((module.finish(), layout)) +} diff --git a/js/src/night/wasm-jit-runner/test.sh b/js/src/night/wasm-jit-runner/test.sh new file mode 100755 index 0000000000000..87bd1ebc7aae3 --- /dev/null +++ b/js/src/night/wasm-jit-runner/test.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Build the runner and the example guest, then run the example end-to-end. +set -e +DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$DIR" + +cargo build +sh guest/example/build.sh +echo "--- running example guest ---" +exec ./target/debug/wasm-jit-runner guest/example/test_guest.wasm diff --git a/js/src/rust/Cargo.toml b/js/src/rust/Cargo.toml index bb396123f5e77..d59a678845624 100644 --- a/js/src/rust/Cargo.toml +++ b/js/src/rust/Cargo.toml @@ -15,6 +15,7 @@ moz_memory = ['mozglue-static/moz_memory'] simd-accel = ['jsrust_shared/simd-accel'] gluesmith = ['jsrust_shared/gluesmith'] icu4x = ['jsrust_shared/icu4x'] +night-compiler = ['jsrust_shared/night-compiler'] [dependencies] mozilla-central-workspace-hack = { version = "0.1", features = ["jsrust"], optional = true } diff --git a/js/src/rust/moz.build b/js/src/rust/moz.build index 3b18ee083be81..d78bea746b75a 100644 --- a/js/src/rust/moz.build +++ b/js/src/rust/moz.build @@ -18,6 +18,9 @@ if CONFIG["MOZ_MEMORY"]: if CONFIG["MOZ_ICU4X"]: features += ["icu4x"] +if CONFIG["ENABLE_JS_NIGHTMONKEY_INPROCESS"]: + features += ["night-compiler"] + RustLibrary("jsrust", features) if CONFIG["JS_SHARED_LIBRARY"]: diff --git a/js/src/rust/shared/Cargo.toml b/js/src/rust/shared/Cargo.toml index 2ab6ea7f32f27..f8ff5695da05d 100644 --- a/js/src/rust/shared/Cargo.toml +++ b/js/src/rust/shared/Cargo.toml @@ -17,10 +17,13 @@ mozglue-static = { path = "../../../../mozglue/static/rust" } gluesmith = { path = "../../fuzz-tests/gluesmith", optional = true } icu_capi = { version = "2.0", optional = true, default-features= false, features = ["any_provider", "compiled_data", "segmenter", "calendar"] } unicode-bidi-ffi = { path = "../../../../intl/bidi/rust/unicode-bidi-ffi" } +night-compiler = { path = "../../night/compiler", optional = true } +night-snapshot = { path = "../../night/snapshot", optional = true } [features] simd-accel = ['encoding_c/simd-accel'] icu4x = ['icu_capi'] +night-compiler = ['dep:night-compiler', 'dep:night-snapshot'] # Uncomment this to enable perf support in release mode. #[profile.release] diff --git a/js/src/rust/shared/lib.rs b/js/src/rust/shared/lib.rs index a2676bc9b341b..bd995254b630c 100644 --- a/js/src/rust/shared/lib.rs +++ b/js/src/rust/shared/lib.rs @@ -17,6 +17,12 @@ extern crate encoding_c_mem; extern crate mozglue_static; extern crate unicode_bidi_ffi; +#[cfg(feature = "night-compiler")] +extern crate night_compiler; + +#[cfg(feature = "night-compiler")] +extern crate night_snapshot; + #[cfg(feature = "gluesmith")] extern crate gluesmith; diff --git a/js/src/shell/CommonShellGlobals.cpp b/js/src/shell/CommonShellGlobals.cpp new file mode 100644 index 0000000000000..12c78a120ce6f --- /dev/null +++ b/js/src/shell/CommonShellGlobals.cpp @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "shell/CommonShellGlobals.h" + +#include + +#include "jsapi.h" // JS_DefineFunction, JS_ValueToSource, JS_ClearPendingException + +#include "js/CallArgs.h" +#include "js/CharacterEncoding.h" // JS_EncodeStringToUTF8 +#include "js/Conversions.h" // JS::ToString +#include "js/Equality.h" // JS::SameValue +#include "js/ErrorReport.h" // JS_ReportErrorUTF8 +#include "js/Printer.h" // js::QuoteString +#include "js/RootingAPI.h" // JS::Rooted +#include "js/Utility.h" // JS::UniqueChars +#include "js/Value.h" + +using namespace JS; + +namespace { + +// Best-effort source representation of a value for error messages. +const char* ValueToSource(JSContext* cx, HandleValue v, UniqueChars* bytes) { + RootedString str(cx, JS_ValueToSource(cx, v)); + if (str) { + *bytes = JS_EncodeStringToUTF8(cx, str); + if (*bytes) { + return bytes->get(); + } + } + JS_ClearPendingException(cx); + return "<>"; +} + +bool Print(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return js::shell::PrintArgs(cx, args, stdout, /* newline = */ true); +} + +} // namespace + +bool js::shell::PrintArgs(JSContext* cx, const CallArgs& args, FILE* out, + bool newline) { + for (unsigned i = 0; i < args.length(); i++) { + RootedString str(cx, ToString(cx, args[i])); + if (!str) { + return false; + } + UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) { + return false; + } + fprintf(out, "%s%s", i ? " " : "", bytes.get()); + } + if (newline) { + fputc('\n', out); + } + fflush(out); + args.rval().setUndefined(); + return true; +} + +bool js::shell::AssertEq(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!(args.length() == 2 || (args.length() == 3 && args[2].isString()))) { + JS_ReportErrorUTF8(cx, "assertEq: %s", + (args.length() < 2) ? "not enough arguments" + : (args.length() == 3) ? "invalid arguments" + : "too many arguments"); + return false; + } + + bool same; + if (!SameValue(cx, args[0], args[1], &same)) { + return false; + } + if (!same) { + UniqueChars bytes0, bytes1; + const char* actual = ValueToSource(cx, args[0], &bytes0); + const char* expected = ValueToSource(cx, args[1], &bytes1); + if (args.length() == 2) { + JS_ReportErrorUTF8(cx, "Assertion failed: got %s, expected %s", actual, + expected); + } else { + RootedString message(cx, args[2].toString()); + UniqueChars bytes2 = js::QuoteString(cx, message); + if (!bytes2) { + return false; + } + JS_ReportErrorUTF8(cx, "Assertion failed: got %s, expected %s: %s", + actual, expected, bytes2.get()); + } + return false; + } + args.rval().setUndefined(); + return true; +} + +bool js::shell::InstallCommonShellGlobals(JSContext* cx, HandleObject global) { + return JS_DefineFunction(cx, global, "print", Print, 0, 0) && + JS_DefineFunction(cx, global, "assertEq", js::shell::AssertEq, 2, 0); +} diff --git a/js/src/shell/CommonShellGlobals.h b/js/src/shell/CommonShellGlobals.h new file mode 100644 index 0000000000000..9647b139200f9 --- /dev/null +++ b/js/src/shell/CommonShellGlobals.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Common shell-style global builtins, factored out of the js shell so that +// minimal embeddings (currently the AOT runtime reactor, js/src/night/runtime/) can +// install the same `print`/`assertEq` the shell exposes without depending on +// the shell program itself. Homed under shell/ so the dependency runs +// embedding -> shell, never the reverse. Self-contained: uses only the public +// JS API (plus js::QuoteString), no shell-only state. + +#ifndef shell_CommonShellGlobals_h +#define shell_CommonShellGlobals_h + +#include + +#include "js/CallArgs.h" +#include "js/TypeDecls.h" + +namespace js { +namespace shell { + +// The shared `print` loop: ToString each argument and write it to `out`, +// space-separated, with a trailing newline when `newline` is set. Sets +// args.rval() to undefined. The shell's redirectable print/printErr wrap this +// (passing their RCFile's stream); InstallCommonShellGlobals uses it for +// stdout. +[[nodiscard]] bool PrintArgs(JSContext* cx, const JS::CallArgs& args, FILE* out, + bool newline); + +// assertEq(actual, expected[, message]): throw if SameValue(actual, expected) +// is false. Error messages match the shell's historical text. +bool AssertEq(JSContext* cx, unsigned argc, JS::Value* vp); + +// Install the modeled common globals on `global`: `print` (-> stdout) and +// `assertEq`. For minimal embeddings; the shell defines its own redirectable +// print but shares PrintArgs/AssertEq above. +[[nodiscard]] bool InstallCommonShellGlobals(JSContext* cx, + JS::HandleObject global); + +} // namespace shell +} // namespace js + +#endif // shell_CommonShellGlobals_h diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index c09a9a4c9c5c1..db6e45918d077 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -172,6 +172,7 @@ #include "js/WasmModule.h" // JS::WasmModule #include "js/Wrapper.h" #include "proxy/DeadObjectProxy.h" // js::IsDeadProxyObject +#include "shell/CommonShellGlobals.h" #include "shell/jsoptparse.h" #include "shell/jsshell.h" #include "shell/OSObject.h" @@ -186,6 +187,10 @@ #include "util/StringBuilder.h" #include "util/Text.h" #include "util/WindowsWrapper.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/Night.h" +# include "night/runtime/NightRegistration.h" +#endif #include "vm/ArgumentsObject.h" #include "vm/Compression.h" #include "vm/ErrorObject.h" @@ -874,6 +879,21 @@ bool shell::OOM_printAllocationCount = false; MOZ_RUNINIT UniqueChars shell::processWideModuleLoadPath; +#ifdef ENABLE_JS_NIGHTMONKEY +// --night-snapshot: compile with full parse and register the script as an +// AOT snapshot root. The top level executes during wizening, so the snapshot +// captures the program's warmed-up class graph and the resumed snapshot +// calls the program's global main(). +static bool enableNightSnapshot = false; +#endif + +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS +// --night-inprocess state: the flag, and whether the unit being compiled is +// the batch candidate (the positional script, or -e code without one). +static bool enableNightInprocess = false; +static bool nightInprocessEligible = false; +#endif + static bool SetTimeoutValue(JSContext* cx, double t); static void KillWatchdog(JSContext* cx); @@ -1313,7 +1333,14 @@ enum class CompileUtf8 { .setIsRunOnce(true) .setNoScriptRval(true); - if (fullParse) { + bool wantFullParse = fullParse; +#ifdef ENABLE_JS_NIGHTMONKEY + wantFullParse |= enableNightSnapshot; +#endif +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + wantFullParse |= enableNightInprocess && nightInprocessEligible; +#endif + if (wantFullParse) { options.setForceFullParse(); } else { options.setEagerDelazificationStrategy(defaultDelazificationMode); @@ -1358,6 +1385,25 @@ enum class CompileUtf8 { return false; } +#ifdef ENABLE_JS_NIGHTMONKEY + if (enableNightSnapshot) { + if (!JS::NightRegisterRoot(cx, script, /* executedAtInit = */ true)) { + return false; + } + if (!js::NightSnapshotCaptureExtras(cx, script)) { + return false; + } + } +#endif + +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + if (enableNightInprocess && nightInprocessEligible) { + if (!js::CompileInProcess(cx, script)) { + return false; + } + } +#endif + #ifdef DEBUG if (dumpEntrainedVariables) { AnalyzeEntrainedVariables(cx, script); @@ -1371,6 +1417,13 @@ enum class CompileUtf8 { if (printTiming) { printf("runtime = %.3f ms\n", double(t2) / PRMJ_USEC_PER_MSEC); } +#ifdef ENABLE_JS_NIGHTMONKEY + // The heap the top level just built is the analysis oracle; capture it + // before the wizer snapshot freezes memory. + if (enableNightSnapshot && !js::NightSnapshotCaptureHeap(cx)) { + return false; + } +#endif } return true; } @@ -3566,23 +3619,8 @@ static bool PrintInternal(JSContext* cx, const CallArgs& args, RCFile* file) { return false; } - for (unsigned i = 0; i < args.length(); i++) { - RootedString str(cx, JS::ToString(cx, args[i])); - if (!str) { - return false; - } - UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); - if (!bytes) { - return false; - } - fprintf(file->fp, "%s%s", i ? " " : "", bytes.get()); - } - - fputc('\n', file->fp); - fflush(file->fp); - - args.rval().setUndefined(); - return true; + // Shared with the AOT runtime via shell/CommonShellGlobals. + return js::shell::PrintArgs(cx, args, file->fp, /* newline = */ true); } static bool Print(JSContext* cx, unsigned argc, Value* vp) { @@ -3677,56 +3715,6 @@ static bool StopTimingMutator(JSContext* cx, unsigned argc, Value* vp) { return true; } -static const char* ToSource(JSContext* cx, HandleValue vp, UniqueChars* bytes) { - RootedString str(cx, JS_ValueToSource(cx, vp)); - if (str) { - *bytes = JS_EncodeStringToUTF8(cx, str); - if (*bytes) { - return bytes->get(); - } - } - JS_ClearPendingException(cx); - return "<>"; -} - -static bool AssertEq(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - if (!(args.length() == 2 || (args.length() == 3 && args[2].isString()))) { - JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, - (args.length() < 2) ? JSSMSG_NOT_ENOUGH_ARGS - : (args.length() == 3) ? JSSMSG_INVALID_ARGS - : JSSMSG_TOO_MANY_ARGS, - "assertEq"); - return false; - } - - bool same; - if (!JS::SameValue(cx, args[0], args[1], &same)) { - return false; - } - if (!same) { - UniqueChars bytes0, bytes1; - const char* actual = ToSource(cx, args[0], &bytes0); - const char* expected = ToSource(cx, args[1], &bytes1); - if (args.length() == 2) { - JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, - JSSMSG_ASSERT_EQ_FAILED, actual, expected); - } else { - RootedString message(cx, args[2].toString()); - UniqueChars bytes2 = QuoteString(cx, message); - if (!bytes2) { - return false; - } - JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, - JSSMSG_ASSERT_EQ_FAILED_MSG, actual, expected, - bytes2.get()); - } - return false; - } - args.rval().setUndefined(); - return true; -} - static JSScript* GetTopScript(JSContext* cx) { NonBuiltinScriptFrameIter iter(cx); return iter.done() ? nullptr : iter.script(); @@ -10073,12 +10061,31 @@ static bool DisableExecutionTracing(JSContext* cx, unsigned argc, #endif // MOZ_EXECUTION_TRACING +static bool NightTierEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); +#ifdef ENABLE_JS_NIGHTMONKEY + bool enabled = js::night::gNightActivated; +# ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + enabled = enabled || enableNightInprocess; +# endif + args.rval().setBoolean(enabled); +#else + args.rval().setBoolean(false); +#endif + return true; +} + // clang-format off static const JSFunctionSpecWithHelp shell_functions[] = { JS_FN_HELP("options", Options, 0, 0, "options([option ...])", " Get or toggle JavaScript options."), + JS_FN_HELP("nightTierEnabled", NightTierEnabled, 0, 0, +"nightTierEnabled()", +" True iff the AOT wasm tier is active in this shell (--night-inprocess, or\n" +" an activated AOT snapshot). Always false in builds without the tier."), + JS_FN_HELP("load", Load, 1, 0, "load(['foo.js' ...])", " Load files named by string arguments. Filename is relative to the\n" @@ -10177,7 +10184,7 @@ static const JSFunctionSpecWithHelp shell_functions[] = { "quit()", " Quit the shell."), - JS_FN_HELP("assertEq", AssertEq, 2, 0, + JS_FN_HELP("assertEq", js::shell::AssertEq, 2, 0, "assertEq(actual, expected[, msg])", " Throw if the first two arguments are not the same (both +0 or both -0,\n" " both NaN, or non-zero and ===)."), @@ -12061,6 +12068,15 @@ auto minVal(T a, Ts... args) { MultiStringRange codeChunks = op->getMultiStringOption('e'); MultiStringRange modulePaths = op->getMultiStringOption('m'); +#ifdef ENABLE_JS_NIGHTMONKEY + enableNightSnapshot = op->getBoolOption("night-snapshot"); + js::night::NightSetWizening(enableNightSnapshot); +#endif + +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + enableNightInprocess = op->getBoolOption("night-inprocess"); +#endif + #ifdef FUZZING_JS_FUZZILLI // Check for REPRL file source if (op->getBoolOption("reprl")) { @@ -12144,9 +12160,29 @@ auto minVal(T a, Ts... args) { } RootedValue rval(cx); +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + // -e code is the batch candidate only when there is no positional + // script (which otherwise claims the single in-process batch). + if (enableNightInprocess && !op->getStringArg("script")) { + opts.setIsRunOnce(true).setNoScriptRval(true); + RootedScript script(cx, JS::Compile(cx, opts, srcBuf)); + if (!script) { + return false; + } + if (!js::CompileInProcess(cx, script)) { + return false; + } + if (!JS_ExecuteScript(cx, script)) { + return false; + } + } else if (!JS::Evaluate(cx, opts, srcBuf, &rval)) { + return false; + } +#else if (!JS::Evaluate(cx, opts, srcBuf, &rval)) { return false; } +#endif codeChunks.popFront(); if (sc->quitting) { @@ -12179,9 +12215,15 @@ auto minVal(T a, Ts... args) { if (!pathUtf8) { return false; } +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + nightInprocessEligible = true; +#endif if (!Process(cx, pathUtf8.get(), false, FileScript)) { return false; } +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + nightInprocessEligible = false; +#endif } if (op->getBoolOption('i')) { @@ -13322,6 +13364,18 @@ bool InitOptionParser(OptionParser& op) { !op.addBoolOption('\0', "wasm-compile-and-serialize", "Compile the wasm bytecode from stdin and serialize " "the results to stdout") || +#ifdef ENABLE_JS_NIGHTMONKEY + !op.addBoolOption('\0', "night-snapshot", + "Register the script as an AOT snapshot root and " + "capture the post-top-level heap, for wizening by " + "the nightmonkey compiler") || +#endif +#ifdef ENABLE_JS_NIGHTMONKEY_INPROCESS + !op.addBoolOption('\0', "night-inprocess", + "AOT-compile the positional script (or -e code) " + "in-process and dispatch into the injected wasm " + "bodies (requires the wasm-jit-runner hostcalls)") || +#endif #ifdef FUZZING_JS_FUZZILLI !op.addBoolOption('\0', "reprl", "Enable REPRL mode for fuzzing") || #endif diff --git a/js/src/shell/moz.build b/js/src/shell/moz.build index e8ca9e67a1e2d..c73af2ded0a82 100644 --- a/js/src/shell/moz.build +++ b/js/src/shell/moz.build @@ -25,6 +25,10 @@ UNIFIED_SOURCES += [ "wizer.cpp", ] +SOURCES += [ + "CommonShellGlobals.cpp", +] + if CONFIG["FUZZING_INTERFACES"]: UNIFIED_SOURCES += ["jsrtfuzzing/jsrtfuzzing.cpp"] if CONFIG["LIBFUZZER"]: @@ -58,7 +62,20 @@ OBJDIR_FILES.js.src += ["!/dist/bin/js%s" % CONFIG["BIN_SUFFIX"]] # Increase the default stack size (64KB) to 1MB. # Also make the stack grow towards 0 so that if SpiderMonkey's stack limiter is buggy, overflow will likely trap. if CONFIG["OS_ARCH"] == "WASI": - LDFLAGS += ["-Wl,-z,stack-size=1048576", "-Wl,--stack-first"] + if CONFIG["ENABLE_JS_NIGHTMONKEY"]: + # The AOT shell needs the memory and funcref table exported (and the + # table growable) so appended/injected code can share them, and a much + # deeper stack: the in-process compiler's Wasm lowering recurses on + # the main stack. + LDFLAGS += [ + "-Wl,-z,stack-size=67108864", + "-Wl,--stack-first", + "-Wl,--export-memory", + "-Wl,--export-table", + "-Wl,--growable-table", + ] + else: + LDFLAGS += ["-Wl,-z,stack-size=1048576", "-Wl,--stack-first"] OS_LIBS += ["wasi-emulated-process-clocks", "wasi-emulated-getpid"] # Make JS Shell builds run without LD_LIBRARY_PATH diff --git a/js/src/shell/wizer.cpp b/js/src/shell/wizer.cpp index 3e0a3fa91a11e..a9c2fe98050b4 100644 --- a/js/src/shell/wizer.cpp +++ b/js/src/shell/wizer.cpp @@ -7,8 +7,13 @@ /* Support for Wizer-based snapshotting of the JS shell, when built * for a Wasm target (i.e., running inside a Wasm module). */ +#include "jsfriendapi.h" // js::RunJobs + #include "js/CallAndConstruct.h" // JS_CallFunctionName #include "shell/jsshell.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/NightRegistration.h" +#endif using namespace js; using namespace js::shell; @@ -20,8 +25,16 @@ using namespace js::shell; static std::optional wizenedContext; static void WizerInit() { + // Wizening a NightMonkey shell exists only to produce an AOT snapshot, so + // the snapshot root registration is always on: the top level runs here, + // during wizening, and the resumed snapshot calls the program's main(). +# ifdef ENABLE_JS_NIGHTMONKEY + const int argc = 2; + char* argv[3] = {strdup("js"), strdup("--night-snapshot"), NULL}; +# else const int argc = 1; char* argv[2] = {strdup("js"), NULL}; +# endif auto ret = ShellMain(argc, argv, /* retainContext = */ true); if (!ret.is()) { @@ -44,13 +57,24 @@ int main(int argc, char** argv) { JSAutoRealm ar(cx, glob); +# ifdef ENABLE_JS_NIGHTMONKEY + JS::NightActivate(cx); +# endif + // Look up a function called "main" in the global. JS::Rooted ret(cx); - if (!JS_CallFunctionName(cx, cx->global(), "main", - JS::HandleValueArray::empty(), &ret)) { + // `glob`, not `cx->global()`: the latter is a Handle, + // which does not convert to the Handle this takes. + if (!JS_CallFunctionName(cx, glob, "main", JS::HandleValueArray::empty(), + &ret)) { fprintf(stderr, "Failed to call main() in Wizened JS source!\n"); abort(); } + // Drain the microtask queue, as the shell's own run loop does after + // every script: a program whose main() leaves promise continuations + // queued (any `async` function, any `.then`) would otherwise exit with + // them unrun -- the work is complete but its result never observed. + js::RunJobs(cx); } else { return ShellMain(argc, argv, /* returnContext = */ false).as(); } diff --git a/js/src/tests/jstests.list b/js/src/tests/jstests.list index 7e29c106b779c..2940baa1bbcb5 100644 --- a/js/src/tests/jstests.list +++ b/js/src/tests/jstests.list @@ -43,6 +43,66 @@ skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/ skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/toLowerCase/special_casing_conditional.js skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/toLocaleLowerCase/Final_Sigma_U180E.js skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/toLocaleLowerCase/special_casing_conditional.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js +skip-if(!this.hasOwnProperty("Intl")) script test262/language/literals/regexp/u-case-mapping.js + +# Unicode property escapes need the Intl property tables. +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/match/regexp-prototype-match-v-u-flag.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/replace/regexp-prototype-replace-v-u-flag.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/search/regexp-prototype-search-v-flag.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/search/regexp-prototype-search-v-u-flag.js + +# The wasi shell's stack is too small for 32 nested function literals. +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script test262/language/statements/function/S13.2.1_A1_T1.js + +# The NightMonkey tier has no frame introspection: fun.caller and +# fun.arguments read null from a compiled frame. +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/language/arguments-object/10.6-13-a-2.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/language/arguments-object/10.6-13-a-3.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/Function/regress-222029-001.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/Function/regress-222029-002.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/Function/regress-85880.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/built-ins/Function/15.3.5.4_2-95gs.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/extensions/arguments-property-access-in-function.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/extensions/function-caller-skips-eval-frames.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/extensions/function-properties.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/regress/regress-577648-1.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/regress/regress-577648-2.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/regress/regress-586482-1.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/regress/regress-586482-2.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/regress/regress-586482-3.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/regress/regress-586482-4.js + +# Compiled frames carry no source positions: Error.stack, lineNumber, +# columnNumber and fileName are empty for them. +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/async-functions/ErrorStack.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/Exceptions/errstack-001.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/Promise/any-stack.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/extensions/column-numbers.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/extensions/errorcolumnblame.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/extensions/regress-50447-1.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/extensions/toSource-infinite-recursion.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/regress/regress-167328.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/template-strings/debugLineNumber.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script shell/script-file-name-utf8.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/extensions/regress-645160.js + +# Error messages from compiled code cannot name the offending expression +# (no bytecode frame to decompile), so the engine falls back to the +# value's source. +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/extensions/regress-353116.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/regress/regress-328664.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/regress/regress-372364.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/regress/regress-420919.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script test262/staging/sm/object/toPrimitive.js + +# loadRelativeToScript resolves against the calling script's frame, which a +# compiled caller does not expose. +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/extensions/clone-v1-typed-array.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/object/15.2.3.6-function-length.js +skip-if(this.hasOwnProperty("nightTierEnabled")&&nightTierEnabled()) script non262/object/15.2.3.6-new-definition.js # Skip intl402 tests when Intl isn't available. skip-if(!this.hasOwnProperty("Intl")) include test262/intl402/jstests.list diff --git a/js/src/vm/BytecodeUtil.cpp b/js/src/vm/BytecodeUtil.cpp index 37e58ff749b65..ae6d1032a19d1 100644 --- a/js/src/vm/BytecodeUtil.cpp +++ b/js/src/vm/BytecodeUtil.cpp @@ -2464,11 +2464,16 @@ static bool DecompileArgumentFromStack(JSContext* cx, int formalIndex, /* * Settle on the nearest script frame, which should be the builtin that - * called the intrinsic. + * called the intrinsic. AOT-compiled frames are invisible to FrameIter, so + * the expected frame may be missing entirely; fall back to no decompiled + * name rather than walking some unrelated frame (or crashing on ++ of a + * done iterator). */ FrameIter frameIter(cx); - MOZ_ASSERT(!frameIter.done()); - MOZ_ASSERT(frameIter.script()->selfHosted()); + if (frameIter.done() || !frameIter.hasScript() || + !frameIter.script()->selfHosted()) { + return true; + } /* * Get the second-to-top frame, the non-self-hosted caller of the builtin diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index bce1ddfe35e38..190bc413ce726 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -78,6 +78,8 @@ MACRO_(caseFirst, "caseFirst") \ MACRO_(catch_, "catch") \ MACRO_(cause, "cause") \ + MACRO_(charAt, "charAt") \ + MACRO_(charCodeAt, "charCodeAt") \ MACRO_(chunks, "chunks") \ MACRO_(class_, "class") \ MACRO_(cleanupSome, "cleanupSome") \ @@ -199,6 +201,7 @@ MACRO_(frame, "frame") \ MACRO_(from, "from") \ MACRO_(fromBase64, "fromBase64") \ + MACRO_(fromCharCode, "fromCharCode") \ MACRO_(fromHex, "fromHex") \ MACRO_(fulfilled, "fulfilled") \ MACRO_(gcCycleNumber, "gcCycleNumber") \ diff --git a/js/src/vm/EnvironmentObject.cpp b/js/src/vm/EnvironmentObject.cpp index d845440e07b9f..98a87df2d90f0 100644 --- a/js/src/vm/EnvironmentObject.cpp +++ b/js/src/vm/EnvironmentObject.cpp @@ -30,6 +30,10 @@ #include "wasm/WasmDebugFrame.h" #include "wasm/WasmInstance.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/NightEnv.h" // night_runtime_global_lexical_shadow_added +#endif + #include "gc/Marking-inl.h" #include "gc/StableCellHasher-inl.h" #include "vm/BytecodeIterator-inl.h" @@ -3907,6 +3911,14 @@ static bool InitGlobalOrEvalDeclarations( attrs)) { return false; } +#ifdef ENABLE_JS_NIGHTMONKEY + // A GLOBAL lexical binding shadows any same-named global-object + // binding for every later read/write: compiled gname caches for the + // name must re-resolve. + if (lexicalEnv->is()) { + night_runtime_global_lexical_shadow_added(id.get().asRawBits()); + } +#endif break; } diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 743716e060285..2b495310c03ca 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -51,6 +51,10 @@ #include "vm/GeneratorObject.h" #include "vm/Iteration.h" #include "vm/JSContext.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/NightEntry.h" // js::night dispatch hooks +# include "night/runtime/NightStack.h" // js::nightrt::AutoNightReentry (AOT dispatch) +#endif #include "vm/JSFunction.h" #include "vm/JSObject.h" #include "vm/JSScript.h" @@ -457,6 +461,17 @@ bool js::RunScript(JSContext* cx, RunState& state) { break; } +#ifdef ENABLE_JS_NIGHTMONKEY + switch (js::night::MaybeEnterNight(cx, state)) { + case js::night::EnterNightStatus::Error: + return false; + case js::night::EnterNightStatus::Ok: + return true; + case js::night::EnterNightStatus::NotEntered: + break; + } +#endif + bool ok = MaybeEnterInterpreterTrampoline(cx, state); if (!ok) { AssertExceptionResult(cx); @@ -3321,6 +3336,21 @@ bool MOZ_NEVER_INLINE JS_HAZ_JSNATIVE_CALLER js::Interpret(JSContext* cx, break; } +#ifdef ENABLE_JS_NIGHTMONKEY + switch (js::night::MaybeEnterNight(cx, args, funScript, + bool(construct))) { + case js::night::EnterNightStatus::Error: + goto error; + case js::night::EnterNightStatus::Ok: + interpReturnOK = true; + CHECK_BRANCH(); + REGS.sp = args.spAfterCall(); + goto jit_return; + case js::night::EnterNightStatus::NotEntered: + break; + } +#endif + #ifdef NIGHTLY_BUILD // If entry trampolines are enabled, call back into // MaybeEnterInterpreterTrampoline so we can generate an @@ -4232,6 +4262,37 @@ bool MOZ_NEVER_INLINE JS_HAZ_JSNATIVE_CALLER js::Interpret(JSContext* cx, END_CASE(CheckResumeKind) CASE(Resume) { +#ifdef ENABLE_JS_NIGHTMONKEY + { + // A generator whose body compiled re-enters its AOT state machine: + // the interpreter cannot resume it, because the saved stack-storage + // layout is the AOT's own. EnterNightResume runs the generator to its + // next suspend point or completion synchronously; the result is the + // yielded/returned value ({value, done} objects are built by the + // generator bytecode). + auto* genRaw = ®S.sp[-3].toObject().as(); + if (js::night::IsNightResumable(genRaw)) { + bool ok; + { + // The rooted scope closes before the dispatch below: an indirect + // goto cannot leave a scope holding non-trivial locals. + Rooted gen(cx, genRaw); + ReservedRooted val(&rootValue0, REGS.sp[-2]); + ReservedRooted resumeKindVal(&rootValue1, REGS.sp[-1]); + // Inputs are rooted here; consume the three operands and write + // the completion value in their place. + REGS.sp -= 2; + ok = js::night::EnterNightResume(cx, gen, val, resumeKindVal, + REGS.stackHandleAt(-1)) == + js::night::EnterNightStatus::Ok; + } + if (!ok) { + goto error; + } + ADVANCE_AND_DISPATCH(JSOpLength_Resume); + } + } +#endif { Rooted gen( cx, ®S.sp[-3].toObject().as()); diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp index 9e8eadd801043..3acdce131684c 100644 --- a/js/src/vm/JSContext.cpp +++ b/js/src/vm/JSContext.cpp @@ -1548,6 +1548,17 @@ void JSContext::trace(JSTracer* trc) { #ifdef ENABLE_WASM_JSPI wasm().promiseIntegration.trace(trc); #endif +#ifdef ENABLE_JS_NIGHTMONKEY + // The AOT value stack is the sole root region for compiled-Wasm frames (their + // args/this/locals/operands are boxed JS::Values living here, not in the GC + // heap). It must be traced on EVERY GC -- like the interpreter and JIT stacks + // -- so a minor (nursery) collection forwards the nursery pointers it holds. + // `JSContext::trace` runs from `traceRuntimeCommon` for both minor and major + // GC; an embedding extra-roots tracer would not (those are major-GC only). + if (nightStack().valid()) { + nightStack().trace(trc); + } +#endif } JS::NativeStackLimit JSContext::stackLimitForJitCode(JS::StackKind kind) { diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h index 5330f54df7daa..e772989b38e0d 100644 --- a/js/src/vm/JSContext.h +++ b/js/src/vm/JSContext.h @@ -371,7 +371,7 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, } // For JIT use. - static size_t offsetOfZone() { return offsetof(JSContext, zone_); } + static constexpr size_t offsetOfZone() { return offsetof(JSContext, zone_); } // Current global. This is only safe to use within the scope of the // AutoRealm from which it's called. @@ -396,7 +396,9 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, JSRuntime* runtime() { return runtime_; } const JSRuntime* runtime() const { return runtime_; } - static size_t offsetOfRealm() { return offsetof(JSContext, realm_); } + static constexpr size_t offsetOfRealm() { + return offsetof(JSContext, realm_); + } friend class JS::AutoSaveExceptionState; friend class js::jit::DebugModeOSRVolatileJitFrameIter; @@ -468,6 +470,9 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, js::InterpreterStack& interpreterStack() { return runtime()->interpreterStack(); } +#ifdef ENABLE_JS_NIGHTMONKEY + js::nightrt::NightStack& nightStack() { return runtime()->nightStack(); } +#endif #ifdef ENABLE_PORTABLE_BASELINE_INTERP js::PortableBaselineStack& portableBaselineStack() { return runtime()->portableBaselineStack(); diff --git a/js/src/vm/JSFunction-inl.h b/js/src/vm/JSFunction-inl.h index 2b003c1ef1d98..8e5b0b606145f 100644 --- a/js/src/vm/JSFunction-inl.h +++ b/js/src/vm/JSFunction-inl.h @@ -15,6 +15,9 @@ #include "vm/JSContext-inl.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/Night.h" // js::night::NightWizening +#endif namespace js { @@ -151,6 +154,13 @@ inline JSAtom* JSFunction::infallibleGetUnresolvedName(JSContext* cx) { size_t propertyCountEstimate = script->immutableScriptData()->propertyCountEstimate; +#ifdef ENABLE_JS_NIGHTMONKEY + if (js::night::NightWizening()) { + propertyCountEstimate = + std::max(propertyCountEstimate, js::night::kWizenThisSlots); + } +#endif + // Choose the alloc assuming at least the default NewObjectKind slots, but // bigger if our estimate shows we need it. allocKind = js::gc::GetGCObjectKind(std::max( diff --git a/js/src/vm/JSObject.cpp b/js/src/vm/JSObject.cpp index 1833bdd89c631..6b041099350c0 100644 --- a/js/src/vm/JSObject.cpp +++ b/js/src/vm/JSObject.cpp @@ -1344,6 +1344,12 @@ void JSObject::swap(JSContext* cx, HandleObject a, HandleObject b, } } +#ifdef ENABLE_JS_NIGHTMONKEY + // Swapped guts may no longer conform to a stamped likely class. + a->clearNightLikelyClass(js::NightBumpSite::ObjectSwap); + b->clearNightLikelyClass(js::NightBumpSite::ObjectSwap); +#endif + // Restore original unique IDs. if ((aid || bid) && (na || nb)) { if ((aid && !gc::SetOrUpdateUniqueId(cx, a, aid)) || diff --git a/js/src/vm/JSObject.h b/js/src/vm/JSObject.h index 84acd43ed81c6..a99fd6077cb6a 100644 --- a/js/src/vm/JSObject.h +++ b/js/src/vm/JSObject.h @@ -87,6 +87,47 @@ bool SetImmutablePrototype(JSContext* cx, JS::HandleObject obj, * as before. * - JSObject::swap() */ +#ifdef ENABLE_JS_NIGHTMONKEY +namespace js { +// Monotone stamp-invalidation epoch: advanced by every action that demotes +// or rewrites an EXISTING object's night class word (claim-bit clears, +// restamps, full clears). Fresh-object stamping does not advance it. An +// unchanged epoch across a call proves every stamp-guarded fact the caller +// held still holds; the night runtime reads it, compiled census builds +// advance it through the census helper for the inline demote arms. +extern uint64_t gNightStampEpoch; + +// Bump-site census hook: called on every ACTUAL epoch bump with the engine +// path that performed it and the class word being demoted. Records into the +// runtime census (kind 66, id = (site << 16) | class idx) when a census +// module is running; near-free otherwise (one null check on the rare bump +// path). Implemented in night/runtime/NightRuntime.cpp. +extern void NightNoteEpochBump(uint32_t site, uint32_t oldWord); + +namespace NightBumpSite { +// clearNightLikelyClass callers. +static constexpr uint32_t ToDictionary = 1; +static constexpr uint32_t ChangeProperty = 2; +static constexpr uint32_t ChangeCustomDataProp = 3; +static constexpr uint32_t RemoveProperty = 4; +static constexpr uint32_t FreezeOrSeal = 5; +static constexpr uint32_t ObjectSwap = 6; +// nightStoreCheckOrClear callers (the setSlot/initSlot value-write chokes). +static constexpr uint32_t StoredValue = 7; +static constexpr uint32_t InitSlotUnchecked = 8; +// setNightClassWord overwriting a real prior stamp (ctor restamp). +static constexpr uint32_t ConstructStamp = 9; +// clearNightSlotsBit callers (NightRuntime add-mismatch paths). +static constexpr uint32_t SlotsAddMismatch = 10; +static constexpr uint32_t SlotsAddMismatch2 = 11; +static constexpr uint32_t SlotsAddMismatch3 = 12; +static constexpr uint32_t SlotsAddMismatch4 = 13; +// JSObject::setFlag (object-flag shape change, e.g. a Watchtower watch). +static constexpr uint32_t ObjectFlagChange = 14; +} // namespace NightBumpSite +} // namespace js +#endif + class JSObject : public js::gc::CellWithTenuredGCPointer { public: @@ -96,8 +137,19 @@ class JSObject // Like shape(), but uses getAtomic to read the header word. js::Shape* shapeMaybeForwarded() const { return headerPtrAtomic(); } +#if defined(ENABLE_JS_NIGHTMONKEY) && defined(JS_64BIT) + // NightMonkey's likely-class word lives in the 32-bit-only alignment pad + // below, so the tier cannot be built 64-bit without growing every object. + // js/moz.configure enforces a wasm32 target; this is the backstop that + // turns a mis-set define into a diagnosable error rather than a confusing + // failure elsewhere. +# error "ENABLE_JS_NIGHTMONKEY requires a 32-bit target (see js/moz.configure)" +#endif + #ifndef JS_64BIT - // Ensure fixed slots have 8-byte alignment on 32-bit platforms. + // Ensure fixed slots have 8-byte alignment on 32-bit platforms. Under + // ENABLE_JS_NIGHTMONKEY this word holds the likely-class word (u16 index + + // u16 flags); 0 means "no likely class". Zeroed at birth in initShape. uint32_t padding_; #endif @@ -151,7 +203,107 @@ class JSObject // shape we still have to initialize. MOZ_ASSERT(Cell::zone() == shape->zone()); initHeaderPtr(shape); +#ifdef ENABLE_JS_NIGHTMONKEY + padding_ = 0; +#endif + } + +#ifdef ENABLE_JS_NIGHTMONKEY + // The likely-class word: u16 stamped layout idx (low) + u16 flags half. + // Flags half: bit 0 (word 0x00010000) = TYPES (every masked layout + // field holds a value of its mask), bit 1 (0x00020000) = SLOTS (the + // static slot predictions are valid for this object; cleared only by + // add mismatches and delete/dictionary paths), bit 14 (0x40000000) = + // RANGES (see below), bit 15 = CONSTRUCTING sentinel, bits 2..13 = + // the alloc site's early class key while the sentinel is set. + // Epoch discipline (all three demote chokes below): a demotion bumps the + // epoch only for a NON-sentinel word. A mid-construction object (bit 31, + // the CONSTRUCTING sentinel) has idx 0, so no compiled guard can pass on + // it and no fact anywhere is predicated on its bits -- clearing them + // invalidates nothing. This mirrors the compiled demote arms' + // `demote_delta` exactly; an unconditional bump here is a standing false + // "stamps broken" signal to every fork's epoch comparison. + void clearNightLikelyClass(uint32_t site = 0) { + if (padding_) { + if (!(padding_ & 0x80000000u)) { + js::gNightStampEpoch++; + js::NightNoteEpochBump(site, padding_); + } + padding_ = 0; + } + } + // SLOTS-only clear: an add deviated from the clump's slot predictions. + void clearNightSlotsBit(uint32_t site = 0) { + if (padding_ & 0x00020000u) { + if (!(padding_ & 0x80000000u)) { + js::gNightStampEpoch++; + js::NightNoteEpochBump(site, padding_); + } + padding_ &= ~0x00020000u; + } + } + // The engine-path VALUE-write choke action. + // + // TYPES asserts per-field NUMBERNESS and nothing finer: a number store + // through ANY path violates no class's claim and keeps the bit. The + // finer per-field mask survives only because every consumer unboxes + // through the number-tag dispatch, i.e. re-checks the mask at the load. + // + // RANGES has no such fallback -- it is consumed CHECKLESSLY, so it + // cannot ride a numberness bit: an engine-path store of a number + // outside the predicted range would keep TYPES and be read back as + // in-range. Only a range-conformant compiled checked store maintains + // it, so every store through here drops it (owner ruling 2026-08-16; + // js/src/night/docs/findings-types-writer-inventory-20260816.md). + // + // Test-then-write so an unstamped receiver -- the common case -- costs + // one load and never dirties the header line. + MOZ_ALWAYS_INLINE void nightStoreCheckOrClear(const JS::Value& v, + uint32_t site = 0) { + uint32_t w = padding_; + if (MOZ_LIKELY((w & (0x00010000u | 0x40000000u)) == 0)) { + return; + } + uint32_t nw = w & ~0x40000000u; + if (!v.isNumber()) { + nw &= ~0x00010000u; + } + if (nw != w) { + if (!(w & 0x80000000u)) { + js::gNightStampEpoch++; + js::NightNoteEpochBump(site, w); + } + padding_ = nw; + } } + // Advance-ineligibility marker (bit 18, the lowest early-key bit, + // dead once stamped): an unpredicted-key add landed beyond the + // object's own layout, so its own prefix predictions still hold + // (SLOTS stays, no epoch bump) but the bit history no longer + // certifies a clump sibling's extension -- the prefix-advance + // restamp declines on it. Only meaningful on a stamped + // (non-sentinel) word. + void setNightAdvIneligible() { padding_ |= 0x00040000u; } + // Construct-time allocation marker without a key: TYPES and RANGES + // seed (their discipline is key-free -- every unchecked write through + // here drops them), SLOTS cannot (adds are uncheckable without a + // layout). + void setNightConstructingSentinel() { padding_ = 0xC0010000; } + // Construct-time allocation word from a resolved `new` site: sentinel + + // early key + optimistic validity bits. The idx half stays 0 -- no + // idx-guarded arm can hit a mid-construction object. + void setNightClassWord(uint32_t w, uint32_t site = 0) { + // A rewrite of a real existing stamp invalidates facts about it; the + // first stamp of a fresh object (word 0 or the CONSTRUCTING sentinel, + // bit 31) invalidates nothing. + if (padding_ != 0 && !(padding_ & 0x80000000u) && padding_ != w) { + js::gNightStampEpoch++; + js::NightNoteEpochBump(site, padding_); + } + padding_ = w; + } + uint32_t nightClassWord() const { return padding_; } +#endif void setShape(js::Shape* shape) { MOZ_ASSERT(maybeCCWRealm() == shape->realm()); setHeaderPtr(shape); diff --git a/js/src/vm/JSScript.cpp b/js/src/vm/JSScript.cpp index 9a5f260cbf621..4a151fd8146ed 100644 --- a/js/src/vm/JSScript.cpp +++ b/js/src/vm/JSScript.cpp @@ -80,6 +80,10 @@ # include "vtune/VTuneWrapper.h" #endif +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/Night.h" // js::night::NightBlowDynamicCodeFuse +#endif + #include "gc/Marking-inl.h" #include "vm/BytecodeIterator-inl.h" #include "vm/BytecodeLocation-inl.h" @@ -1678,6 +1682,14 @@ bool ScriptSource::assignSource(FrontendContext* fc, MOZ_ASSERT(data.is(), "source assignment should only occur on fresh ScriptSources"); +#ifdef ENABLE_JS_NIGHTMONKEY + // Source text is entering the frontend, and this is the one place every + // compile-from-source passes through (delazification reuses an existing + // ScriptSource and does not come here). Blow before anything can run, and + // before the option-dependent early returns below. + js::night::NightBlowDynamicCodeFuse(); +#endif + mutedErrors_ = options.mutedErrors(); delazificationMode_ = options.eagerDelazificationStrategy(); diff --git a/js/src/vm/JSScript.h b/js/src/vm/JSScript.h index bed6c7d01f055..c4956600da06a 100644 --- a/js/src/vm/JSScript.h +++ b/js/src/vm/JSScript.h @@ -1411,6 +1411,12 @@ class alignas(uintptr_t) PrivateScriptData final return sizeof(PrivateScriptData); } +#ifdef ENABLE_JS_NIGHTMONKEY + static constexpr size_t offsetOfNGCThings() { + return offsetof(PrivateScriptData, ngcthings); + } +#endif + // Accessors for typed array spans. mozilla::Span gcthings() { Offset offset = offsetOfGCThings(); @@ -1576,6 +1582,14 @@ class BaseScript : public gc::TenuredCellWithNonGCPointer { UniquePtr weval_ = {}; #endif +#ifdef ENABLE_JS_NIGHTMONKEY + // Index into the AOT runtime's __indirect_function_table of this script's + // AOT-compiled Wasm body, or 0 when there is none (the script runs + // interpreted). The engine call path call_indirects through this with the + // §1 ABI. See js/src/night/aot-codegen-spec.md sections 1/4.1. + uint32_t nightFuncIndex_ = 0; +#endif + // End of fields. BaseScript(uint8_t* stubEntry, JSFunction* function, @@ -1599,6 +1613,17 @@ class BaseScript : public gc::TenuredCellWithNonGCPointer { bool isUsingInterpreterTrampoline(JSRuntime* rt) const; +#ifdef ENABLE_JS_NIGHTMONKEY + uint32_t nightFuncIndex() const { return nightFuncIndex_; } + void setNightFuncIndex(uint32_t index) { nightFuncIndex_ = index; } + // Offset of `nightFuncIndex_`, baked by the driver's inline call-classify + // (translate.rs) and release-asserted at reactor startup. offsetof is not + // usable in a constant expression here, so the check is at runtime. + static constexpr size_t offsetOfNightFuncIndex() { + return offsetof(BaseScript, nightFuncIndex_); + } +#endif + // Canonical function for the script, if it has a function. For top-level // scripts this is nullptr. JSFunction* function() const { return function_; } @@ -1751,6 +1776,11 @@ class BaseScript : public gc::TenuredCellWithNonGCPointer { static constexpr size_t offsetOfWarmUpData() { return offsetof(BaseScript, warmUpData_); } +#ifdef ENABLE_JS_NIGHTMONKEY + static constexpr size_t offsetOfFunction() { + return offsetof(BaseScript, function_); + } +#endif #if defined(DEBUG) || defined(JS_JITSPEW) void dumpStringContent(js::GenericPrinter& out) const; diff --git a/js/src/vm/MatchPairs.h b/js/src/vm/MatchPairs.h index 6bb60b46ddb3d..961b55450a422 100644 --- a/js/src/vm/MatchPairs.h +++ b/js/src/vm/MatchPairs.h @@ -125,6 +125,15 @@ class VectorMatchPairs : public MatchPairs { protected: friend class RegExpShared; friend class RegExpStatics; +#ifdef ENABLE_JS_NIGHTMONKEY + // The collapsed AOT regexp fast paths allocate their own pairs without + // going through RegExpShared::execute. + friend bool NightRegExpBuiltinFast(JSContext* cx, JS::Value* frame, + unsigned argc, bool searcher, + bool* handled); + friend bool NightRegExpExecTestFast(JSContext* cx, JS::Value* frame, + bool forTest, bool* handled); +#endif /* MatchPair buffer allocator: set pairs_ and pairCount_. */ bool allocOrExpandArray(size_t pairCount); diff --git a/js/src/vm/NativeObject.cpp b/js/src/vm/NativeObject.cpp index 6297e6f396aef..aeb526d59ae4d 100644 --- a/js/src/vm/NativeObject.cpp +++ b/js/src/vm/NativeObject.cpp @@ -24,6 +24,11 @@ #include "vm/PlainObject.h" // js::PlainObject #include "vm/TypedArrayObject.h" #include "vm/Watchtower.h" + +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/Night.h" // js::night::NightAddPropCheck +# include "vm/GlobalObject.h" +#endif #include "gc/Nursery-inl.h" #include "vm/JSObject-inl.h" #include "vm/Shape-inl.h" @@ -1475,6 +1480,9 @@ bool js::AddSlotAndCallAddPropHook(JSContext* cx, Handle obj, return false; } obj->initSlot(slot, v); +#ifdef ENABLE_JS_NIGHTMONKEY + night::NightAddPropCheck(obj, id, slot, obj->numFixedSlots()); +#endif if (MOZ_UNLIKELY(hasUnpreservedWrapper)) { MaybePreserveDOMWrapper(cx, obj); @@ -1589,6 +1597,11 @@ bool js::NativeDefineProperty(JSContext* cx, Handle obj, HandleId id, Handle desc_, ObjectOpResult& result) { desc_.assertValid(); +#ifdef ENABLE_JS_NIGHTMONKEY + if (MOZ_UNLIKELY(obj->is())) { + night::NightGlobalKeyBlow(id); + } +#endif // Section numbers and step numbers below refer to ES2025, draft rev // ac21460fedf4b926520b06c9820bdbebad596a8b. @@ -2446,6 +2459,11 @@ static bool NativeSetExistingDataProperty(JSContext* cx, if (prop.isDataProperty()) { // The common path. Standard data property. +#ifdef ENABLE_JS_NIGHTMONKEY + if (MOZ_UNLIKELY(obj->is())) { + night::NightGlobalDataStore(id, v.asRawBits()); + } +#endif obj->setSlot(prop.slot(), v); return result.succeed(); } @@ -2865,6 +2883,11 @@ bool js::NativeDeleteProperty(JSContext* cx, Handle obj, result)) { return false; } +#ifdef ENABLE_JS_NIGHTMONKEY + if (MOZ_UNLIKELY(obj->is())) { + night::NightGlobalKeyBlow(id); + } +#endif if (!result) { return true; } diff --git a/js/src/vm/NativeObject.h b/js/src/vm/NativeObject.h index 74eb0d0daa9bf..f5d8b93e79222 100644 --- a/js/src/vm/NativeObject.h +++ b/js/src/vm/NativeObject.h @@ -361,18 +361,18 @@ class ObjectElements { bool isSharedMemory() const { return flags & SHARED_MEMORY; } - static int offsetOfFlags() { + static constexpr int offsetOfFlags() { return int(offsetof(ObjectElements, flags)) - int(sizeof(ObjectElements)); } - static int offsetOfInitializedLength() { + static constexpr int offsetOfInitializedLength() { return int(offsetof(ObjectElements, initializedLength)) - int(sizeof(ObjectElements)); } - static int offsetOfCapacity() { + static constexpr int offsetOfCapacity() { return int(offsetof(ObjectElements, capacity)) - int(sizeof(ObjectElements)); } - static int offsetOfLength() { + static constexpr int offsetOfLength() { return int(offsetof(ObjectElements, length)) - int(sizeof(ObjectElements)); } @@ -1170,6 +1170,12 @@ class NativeObject : public JSObject { MOZ_ASSERT(AtomIsMarked(zoneFromAnyThread(), v)); MOZ_ASSERT_IF(v.isMagic() && v.whyMagic() == JS_ELEMENTS_HOLE, !denseElementsArePacked()); +#ifdef ENABLE_JS_NIGHTMONKEY + // The shallow-conformance choke point all setSlot/initSlot flavors + // funnel through: number stores keep the flags (the claim is + // numberness); any other store clears them. + nightStoreCheckOrClear(v, js::NightBumpSite::StoredValue); +#endif } MOZ_ALWAYS_INLINE void setSlot(uint32_t slot, const Value& value) { @@ -1186,6 +1192,9 @@ class NativeObject : public JSObject { } MOZ_ALWAYS_INLINE void initSlotUnchecked(uint32_t slot, const Value& value) { +#ifdef ENABLE_JS_NIGHTMONKEY + nightStoreCheckOrClear(value, js::NightBumpSite::InitSlotUnchecked); +#endif getSlotAddressUnchecked(slot)->init(this, HeapSlot::Slot, slot, value); } @@ -1743,7 +1752,9 @@ class NativeObject : public JSObject { } /* JIT Accessors */ - static size_t offsetOfElements() { return offsetof(NativeObject, elements_); } + static constexpr size_t offsetOfElements() { + return offsetof(NativeObject, elements_); + } static size_t offsetOfFixedElements() { return sizeof(NativeObject) + sizeof(ObjectElements); } @@ -1763,7 +1774,9 @@ class NativeObject : public JSObject { MOZ_ASSERT(offset % sizeof(Value) == 0); return offset / sizeof(Value); } - static size_t offsetOfSlots() { return offsetof(NativeObject, slots_); } + static constexpr size_t offsetOfSlots() { + return offsetof(NativeObject, slots_); + } }; inline void NativeObject::privatePreWriteBarrier(HeapSlot* pprivate) { diff --git a/js/src/vm/RealmFuses.cpp b/js/src/vm/RealmFuses.cpp index 33369ccb99b35..f15d7ebd7f1aa 100644 --- a/js/src/vm/RealmFuses.cpp +++ b/js/src/vm/RealmFuses.cpp @@ -10,6 +10,9 @@ #include "builtin/MapObject.h" #include "builtin/Promise.h" #include "builtin/RegExp.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "builtin/String.h" +#endif #include "builtin/WeakMapObject.h" #include "builtin/WeakSetObject.h" #include "js/experimental/TypedData.h" @@ -643,3 +646,25 @@ bool js::OptimizeWeakSetPrototypeAddFuse::checkInvariant(JSContext* cx) { return ObjectHasDataPropertyFunction(proto, NameToId(cx->names().add), WeakSetObject::add); } + +#ifdef ENABLE_JS_NIGHTMONKEY +bool js::OptimizeStringCharOpsFuse::checkInvariant(JSContext* cx) { + auto* proto = cx->global()->maybeGetPrototype(JSProto_String); + if (!proto) { + // No proto, invariant still holds + return true; + } + if (!ObjectHasDataPropertyFunction(proto, NameToId(cx->names().charCodeAt), + js::str_charCodeAt) || + !ObjectHasDataPropertyFunction(proto, NameToId(cx->names().charAt), + js::str_charAt)) { + return false; + } + auto* ctor = cx->global()->maybeGetConstructor(JSProto_String); + if (!ctor) { + return true; + } + return ObjectHasDataPropertyFunction(ctor, NameToId(cx->names().fromCharCode), + js::str_fromCharCode); +} +#endif // ENABLE_JS_NIGHTMONKEY diff --git a/js/src/vm/RealmFuses.h b/js/src/vm/RealmFuses.h index decb0b2763b79..1e893380e4216 100644 --- a/js/src/vm/RealmFuses.h +++ b/js/src/vm/RealmFuses.h @@ -311,6 +311,28 @@ struct OptimizeWeakSetPrototypeAddFuse final : public RealmFuse { virtual bool checkInvariant(JSContext* cx) override; }; +#ifdef ENABLE_JS_NIGHTMONKEY +// Fuse guarding the original String char-op natives. If this fuse is intact, +// the following invariants must hold: +// +// - The builtin `String.prototype` object has unchanged `charCodeAt` and +// `charAt` data properties (the original natives). +// - The builtin `String` constructor has an unchanged `fromCharCode` data +// property. +// +// Its only consumer is the AOT runtime's inline string char-op fast paths, +// which is why it is not read by CacheIR. +struct OptimizeStringCharOpsFuse final : public RealmFuse { + virtual const char* name() override { return "OptimizeStringCharOpsFuse"; } + virtual bool checkInvariant(JSContext* cx) override; +}; + +# define FOR_EACH_NIGHTMONKEY_REALM_FUSE(FUSE) \ + FUSE(OptimizeStringCharOpsFuse, optimizeStringCharOpsFuse) +#else +# define FOR_EACH_NIGHTMONKEY_REALM_FUSE(FUSE) +#endif + #define FOR_EACH_REALM_FUSE(FUSE) \ FUSE(OptimizeGetIteratorFuse, optimizeGetIteratorFuse) \ FUSE(OptimizeArrayIteratorPrototypeFuse, optimizeArrayIteratorPrototypeFuse) \ @@ -336,7 +358,8 @@ struct OptimizeWeakSetPrototypeAddFuse final : public RealmFuse { FUSE(OptimizeMapPrototypeSetFuse, optimizeMapPrototypeSetFuse) \ FUSE(OptimizeSetPrototypeAddFuse, optimizeSetPrototypeAddFuse) \ FUSE(OptimizeWeakMapPrototypeSetFuse, optimizeWeakMapPrototypeSetFuse) \ - FUSE(OptimizeWeakSetPrototypeAddFuse, optimizeWeakSetPrototypeAddFuse) + FUSE(OptimizeWeakSetPrototypeAddFuse, optimizeWeakSetPrototypeAddFuse) \ + FOR_EACH_NIGHTMONKEY_REALM_FUSE(FUSE) struct RealmFuses { RealmFuses() = default; diff --git a/js/src/vm/RegExpObject.cpp b/js/src/vm/RegExpObject.cpp index 5d32fea550687..19dee4cb02739 100644 --- a/js/src/vm/RegExpObject.cpp +++ b/js/src/vm/RegExpObject.cpp @@ -724,6 +724,20 @@ RegExpRunStatus RegExpShared::execute(JSContext* cx, return RegExpRunStatus::Error; } +#ifdef ENABLE_JS_NIGHTMONKEY + // AOT wasm matcher: decide the match here, skipping the irregexp + // jit-choice + interpreter layering below. Falls through on RETRY + // (backtrack budget / stack limit) or when no matcher covers this + // pattern/encoding. + { + RegExpRunStatus nightStatus; + if (irregexp::TryNightRegexMatch(cx, re, input, start, matches, + input->hasLatin1Chars(), &nightStatus)) { + return nightStatus; + } + } +#endif + uint32_t interruptRetries = 0; const uint32_t maxInterruptRetries = 4; do { diff --git a/js/src/vm/RegExpShared.h b/js/src/vm/RegExpShared.h index 99f048a5eabd0..7833a9d305207 100644 --- a/js/src/vm/RegExpShared.h +++ b/js/src/vm/RegExpShared.h @@ -121,6 +121,12 @@ class RegExpShared uint32_t maxRegisters_ = 0; uint32_t ticks_ = 0; +#ifdef ENABLE_JS_NIGHTMONKEY + // Cached AOT wasm-matcher lookup: UINT32_MAX unresolved, 0 no matcher, + // else index+1 into the runtime nightData().regexTable. + uint32_t nightRegexEntryPlus1_ = UINT32_MAX; +#endif + // With duplicate named capture groups, it's possible that the number of // distinct named groups is less than the total number of named captures. // If they are equal, we used the namedCaptureIndices_ array directly to @@ -216,6 +222,11 @@ class RegExpShared maxRegisters_ = std::max(maxRegisters_, numRegisters); } +#ifdef ENABLE_JS_NIGHTMONKEY + uint32_t nightRegexEntryPlus1() const { return nightRegexEntryPlus1_; } + void setNightRegexEntryPlus1(uint32_t v) { nightRegexEntryPlus1_ = v; } +#endif + uint32_t numNamedCaptures() const { return numNamedCaptures_; } uint32_t numDistinctNamedCaptures() const { return numDistinctNamedCaptures_; diff --git a/js/src/vm/RegExpStatics.h b/js/src/vm/RegExpStatics.h index f34ae2dc9a96a..f168da727ca8a 100644 --- a/js/src/vm/RegExpStatics.h +++ b/js/src/vm/RegExpStatics.h @@ -10,10 +10,15 @@ #include "js/RegExpFlags.h" #include "vm/JSContext.h" #include "vm/MatchPairs.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "vm/RegExpShared.h" // used only by updateLazily (AOT-only) +#endif #include "vm/Runtime.h" namespace js { +class RegExpShared; + class RegExpStatics { /* The latest RegExp output, set after execution. */ VectorMatchPairs matches; @@ -59,6 +64,17 @@ class RegExpStatics { inline bool updateFromMatchPairs(JSContext* cx, JSLinearString* input, VectorMatchPairs& newPairs); +#ifdef ENABLE_JS_NIGHTMONKEY + // Lazy update: record only what executeLazy needs to replay the match on + // the first statics read (source atom, flags, start index, input), + // skipping the per-match pairs copy. Only valid for a match that + // SUCCEEDED with exactly these arguments -- executeLazy asserts the + // replay succeeds. This is the cheap scheme the AOT runtime uses on its + // per-match paths. + inline void updateLazily(JSContext* cx, JSLinearString* input, + RegExpShared* shared, size_t lastIndex); +#endif + inline void clear(); /* Corresponds to JSAPI functionality to set the pending RegExp input. */ @@ -233,6 +249,20 @@ inline bool RegExpStatics::createRightContext(JSContext* cx, return createDependent(cx, matches[0].limit, matchesInput->length(), out); } +#ifdef ENABLE_JS_NIGHTMONKEY +inline void RegExpStatics::updateLazily(JSContext* cx, JSLinearString* input, + RegExpShared* shared, + size_t lastIndex) { + MOZ_ASSERT(input && shared); + BarrieredSetPair(cx->zone(), pendingInput, input, + matchesInput, input); + lazySource = shared->getSource(); + lazyFlags = shared->getFlags(); + lazyIndex = lastIndex; + pendingLazyEvaluation = 1; +} +#endif + inline bool RegExpStatics::updateFromMatchPairs(JSContext* cx, JSLinearString* input, VectorMatchPairs& newPairs) { diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index e7d9e55422e0c..d1f6cc51f771c 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -61,6 +61,11 @@ #include "vm/Stack.h" #include "wasm/WasmTypeDecls.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/NightRuntimeData.h" // js::night::NightRuntimeData +# include "night/runtime/NightStack.h" // js::nightrt::NightStack +#endif + struct JSAtomState; struct JSClass; struct JSErrorInterceptor; @@ -317,8 +322,21 @@ struct JSRuntime { js::MainThreadData portableBaselineStack_; #endif +#ifdef ENABLE_JS_NIGHTMONKEY + /* The AOT value stack: sole GC root for AOT-compiled code + * (js/src/night/runtime). */ + js::MainThreadData nightStack_; + /* Per-runtime caches for AOT-synthesized objects (anonymous-slot keys and + * memoized N-slot shapes). */ + js::MainThreadData nightData_; +#endif + public: js::InterpreterStack& interpreterStack() { return interpreterStack_.ref(); } +#ifdef ENABLE_JS_NIGHTMONKEY + js::nightrt::NightStack& nightStack() { return nightStack_.ref(); } + js::night::NightRuntimeData& nightData() { return nightData_.ref(); } +#endif #ifdef ENABLE_PORTABLE_BASELINE_INTERP js::PortableBaselineStack& portableBaselineStack() { return portableBaselineStack_.ref(); diff --git a/js/src/vm/Scope.h b/js/src/vm/Scope.h index 8c4bc90f6cd6f..08f5686292d9b 100644 --- a/js/src/vm/Scope.h +++ b/js/src/vm/Scope.h @@ -383,6 +383,16 @@ class Scope : public gc::TenuredCellWithNonGCPointer { ScopeKind kind() const { return kind_; } +#ifdef ENABLE_JS_NIGHTMONKEY + static constexpr size_t offsetOfKind() { return offsetof(Scope, kind_); } + static constexpr size_t offsetOfEnvironmentShape() { + return offsetof(Scope, environmentShape_); + } + static constexpr size_t offsetOfEnclosingScope() { + return offsetof(Scope, enclosingScope_); + } +#endif + bool isNamedLambda() const { return kind() == ScopeKind::NamedLambda || kind() == ScopeKind::StrictNamedLambda; diff --git a/js/src/vm/Shape.cpp b/js/src/vm/Shape.cpp index 87d89b5f1dc9a..222aaaa573f22 100644 --- a/js/src/vm/Shape.cpp +++ b/js/src/vm/Shape.cpp @@ -16,6 +16,10 @@ #include "vm/ShapeZone.h" #include "vm/Watchtower.h" +#ifdef ENABLE_JS_NIGHTMONKEY +# include "night/runtime/Night.h" // js::night::NightAddPropCheck +#endif + #include "gc/StableCellHasher-inl.h" #include "vm/JSContext-inl.h" #include "vm/JSObject-inl.h" @@ -130,6 +134,10 @@ bool js::NativeObject::toDictionaryMode(JSContext* cx, obj->setShape(shape); +#ifdef ENABLE_JS_NIGHTMONKEY + obj->clearNightLikelyClass(js::NightBumpSite::ToDictionary); +#endif + MOZ_ASSERT(obj->inDictionaryMode()); obj->setDictionaryModeSlotSpan(span); @@ -340,7 +348,13 @@ bool NativeObject::addProperty(JSContext* cx, Handle obj, } if (auto* shape = LookupShapeForAdd(obj->shape(), id, flags, slot)) { - return obj->setShapeAndAddNewSlot(cx, shape, *slot); + if (!obj->setShapeAndAddNewSlot(cx, shape, *slot)) { + return false; + } +#ifdef ENABLE_JS_NIGHTMONKEY + night::NightAddPropCheck(obj, id, *slot, obj->numFixedSlots()); +#endif + return true; } if (obj->inDictionaryMode()) { @@ -390,6 +404,9 @@ bool NativeObject::addProperty(JSContext* cx, Handle obj, if (!obj->setShapeAndAddNewSlot(cx, newShape, *slot)) { return false; } +#ifdef ENABLE_JS_NIGHTMONKEY + night::NightAddPropCheck(obj, id, *slot, obj->numFixedSlots()); +#endif // Add the new shape to the old shape's shape cache, to optimize this shape // transition. Don't do this if we just allocated a new shape, because that @@ -523,6 +540,10 @@ bool NativeObject::changeProperty(JSContext* cx, Handle obj, uint32_t* slotOut) { MOZ_ASSERT(!id.isVoid()); +#ifdef ENABLE_JS_NIGHTMONKEY + obj->clearNightLikelyClass(js::NightBumpSite::ChangeProperty); +#endif + AutoCheckShapeConsistency check(obj); AssertValidArrayIndex(obj, id); MOZ_ASSERT(!flags.isCustomDataProperty(), @@ -696,6 +717,10 @@ bool NativeObject::changeCustomDataPropAttributes(JSContext* cx, AssertValidArrayIndex(obj, id); AssertValidCustomDataProp(obj, flags); +#ifdef ENABLE_JS_NIGHTMONKEY + obj->clearNightLikelyClass(js::NightBumpSite::ChangeCustomDataProp); +#endif + Rooted map(cx, obj->shape()->propMap()); uint32_t mapLength = obj->shape()->propMapLength(); @@ -839,6 +864,10 @@ bool NativeObject::removeProperty(JSContext* cx, Handle obj, HandleId id) { AutoCheckShapeConsistency check(obj); +#ifdef ENABLE_JS_NIGHTMONKEY + obj->clearNightLikelyClass(js::NightBumpSite::RemoveProperty); +#endif + Rooted map(cx, obj->shape()->propMap()); uint32_t mapLength = obj->shape()->propMapLength(); @@ -1008,6 +1037,10 @@ bool NativeObject::freezeOrSealProperties(JSContext* cx, IntegrityLevel level) { AutoCheckShapeConsistency check(obj); +#ifdef ENABLE_JS_NIGHTMONKEY + obj->clearNightLikelyClass(js::NightBumpSite::FreezeOrSeal); +#endif + if (!Watchtower::watchFreezeOrSeal(cx, obj, level)) { return false; } @@ -1077,6 +1110,15 @@ bool JSObject::setFlag(JSContext* cx, HandleObject obj, ObjectFlag flag) { ObjectFlags objectFlags = obj->shape()->objectFlags(); objectFlags.setFlag(flag); +#ifdef ENABLE_JS_NIGHTMONKEY + // An object flag changes the object's semantics for compiled fast paths + // (a Watchtower watch expects notifications the inline stamped-set arms + // never send; NotExtensible/Frozen gate adds and writes). Demote so + // every compiled arm falls back to the engine paths that respect the + // flag. Rare by construction -- flags are set once per object. + obj->clearNightLikelyClass(js::NightBumpSite::ObjectFlagChange); +#endif + uint32_t numFixed = obj->is() ? obj->as().numFixedSlots() : 0; return Shape::replaceShape(cx, obj, objectFlags, obj->shape()->proto(), diff --git a/js/src/vm/Shape.h b/js/src/vm/Shape.h index e2e2f09966aba..4ddbea5da1d02 100644 --- a/js/src/vm/Shape.h +++ b/js/src/vm/Shape.h @@ -459,7 +459,7 @@ class Shape : public gc::CellWithTenuredGCPointer { return offsetof(Shape, objectFlags_); } - static inline size_t offsetOfImmutableFlags() { + static constexpr size_t offsetOfImmutableFlags() { return offsetof(Shape, immutableFlags); } diff --git a/js/src/vm/SharedStencil.h b/js/src/vm/SharedStencil.h index a402895bd658d..5eaca8b18a749 100644 --- a/js/src/vm/SharedStencil.h +++ b/js/src/vm/SharedStencil.h @@ -619,6 +619,17 @@ class alignas(uint32_t) ImmutableScriptData final "JIT expect Offset to be uint32_t"); return offsetof(ImmutableScriptData, optArrayOffset_); } +#ifdef ENABLE_JS_NIGHTMONKEY + static constexpr size_t offsetOfCodeLength() { + return offsetof(ImmutableScriptData, codeLength_); + } + static constexpr size_t offsetOfMainOffset() { + return offsetof(ImmutableScriptData, mainOffset); + } + static constexpr size_t offsetOfBodyScopeIndex() { + return offsetof(ImmutableScriptData, bodyScopeIndex); + } +#endif static constexpr size_t offsetOfNfixed() { return offsetof(ImmutableScriptData, nfixed); } diff --git a/js/src/vm/StaticStrings.h b/js/src/vm/StaticStrings.h index e34df50909415..d4d954a2e2512 100644 --- a/js/src/vm/StaticStrings.h +++ b/js/src/vm/StaticStrings.h @@ -97,6 +97,13 @@ class StaticStrings { return unitStaticTable[c]; } +#ifdef ENABLE_JS_NIGHTMONKEY + // AOT wasm codegen (js/src/night/runtime): the raw unit-string table base, published + // into linear memory at startup so compiled code can inline s[i] for linear + // latin1 strings (every latin1 char has a static unit string). + JSAtom* const* unitStaticTableBase() const { return unitStaticTable; } +#endif + /* May not return atom, returns null on (reported) failure. */ inline JSLinearString* getUnitString(JSContext* cx, char16_t c); diff --git a/js/src/vm/Watchtower.cpp b/js/src/vm/Watchtower.cpp index bcebe2b23f101..93f0eedeaef11 100644 --- a/js/src/vm/Watchtower.cpp +++ b/js/src/vm/Watchtower.cpp @@ -421,6 +421,30 @@ static void MaybePopWeakSetPrototypeFuses(JSContext* cx, NativeObject* obj, } } +#ifdef ENABLE_JS_NIGHTMONKEY +static void MaybePopStringPrototypeFuses(JSContext* cx, NativeObject* obj, + jsid id) { + if (obj != obj->global().maybeGetPrototype(JSProto_String)) { + return; + } + if (id.isAtom(cx->names().charCodeAt) || id.isAtom(cx->names().charAt)) { + obj->realm()->realmFuses.optimizeStringCharOpsFuse.popFuse( + cx, obj->realm()->realmFuses); + } +} + +static void MaybePopStringConstructorFuses(JSContext* cx, NativeObject* obj, + jsid id) { + if (obj != obj->global().maybeGetConstructor(JSProto_String)) { + return; + } + if (id.isAtom(cx->names().fromCharCode)) { + obj->realm()->realmFuses.optimizeStringCharOpsFuse.popFuse( + cx, obj->realm()->realmFuses); + } +} +#endif // ENABLE_JS_NIGHTMONKEY + static void MaybePopPromiseConstructorFuses(JSContext* cx, NativeObject* obj, jsid id) { if (obj != obj->global().maybeGetConstructor(JSProto_Promise)) { @@ -564,6 +588,12 @@ static void MaybePopRealmFuses(JSContext* cx, NativeObject* obj, jsid id) { // Handle writes to WeakSet.prototype fuse properties. MaybePopWeakSetPrototypeFuses(cx, obj, id); +#ifdef ENABLE_JS_NIGHTMONKEY + // Handle writes to String.prototype / String constructor fuse properties. + MaybePopStringPrototypeFuses(cx, obj, id); + MaybePopStringConstructorFuses(cx, obj, id); +#endif + // Handle writes to Promise constructor fuse properties. MaybePopPromiseConstructorFuses(cx, obj, id); From 3dce3b6bd9d4f18ca42207e83095fcf02b74440d Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Fri, 4 Sep 2026 22:03:14 -0700 Subject: [PATCH 3/7] Run jit-tests / jstests under NightMonkey in CI. --- .github/workflows/main.yml | 8 +- js/src/night/README.md | 10 ++- js/src/night/build_wasm_jit_runner.py | 9 ++ js/src/night/inproc-shell.sh | 8 +- js/src/tests/jstests.list | 120 +++++++++++++++++++++++++- 5 files changed, 147 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0840e00500ec4..3ecb45431733d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,18 +8,20 @@ defaults: run: shell: bash -jobs: +jobs: test: strategy: fail-fast: false matrix: test: [jstests, jit-test] - mozconfig: [ debug, pbl-debug, pbl-release, release, aot-ics-release ] + mozconfig: [ debug, pbl-debug, pbl-release, release, aot-ics-release, nightmonkey ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: submodules: true - run: ./mach --no-interactive bootstrap --application-choice=js + - if: matrix.mozconfig == 'nightmonkey' + run: rustup target add wasm32-wasip1 - run: MOZCONFIG=.github/workflows/mozconfig-${{ matrix.mozconfig }} ./mach build - - run: MOZCONFIG=.github/workflows/mozconfig-${{ matrix.mozconfig }} ./mach ${{ matrix.test }} ${{ matrix.test == 'jit-test' && '--exclude wasm/atomicity.js' || '' }} + - run: MOZCONFIG=.github/workflows/mozconfig-${{ matrix.mozconfig }} ./mach ${{ matrix.test }} ${{ matrix.mozconfig == 'nightmonkey' && '--shell obj-nightmonkey-inprocess/dist/bin/inproc-shell.sh' || '' }} ${{ matrix.test == 'jit-test' && '--exclude wasm/atomicity.js' || '' }} diff --git a/js/src/night/README.md b/js/src/night/README.md index 9f3ec94502c0d..792645253ece2 100644 --- a/js/src/night/README.md +++ b/js/src/night/README.md @@ -79,7 +79,7 @@ Block Versioning, but that did not converge well.) | `configs/` | The mozconfigs (see "Builds"). | | `docs/` | `DESIGN.md`, `INTEGRATION.md`, `TODO`. | | `tools/` | Profiling, benchmarking, and visualization helpers (`viz.py`, `opprof.py`, `pairab.sh`, ...). | -| `inproc-shell.sh` | `jit_test.py` shim running the wasm shell under the runner. | +| `inproc-shell.sh` | `jit_test.py`/`jstests.py` shim running the wasm shell under the runner; the in-process build installs a copy into `dist/bin`. | ## Build flags @@ -173,6 +173,14 @@ python3 js/src/jit-test/jit_test.py -j16 js/src/night/inproc-shell.sh NIGHT_INPROCESS_OFF=1 python3 js/src/jit-test/jit_test.py -j16 js/src/night/inproc-shell.sh ``` +The same shim serves jstests. Through mach, the installed copy stands in for +the shell (the jstests harness finds the objdir from its path): + +``` +MOZCONFIG=js/src/night/configs/mozconfig-nightmonkey-inprocess \ + ./mach jstests --shell obj-nightmonkey-inprocess/dist/bin/inproc-shell.sh +``` + Both lanes are expected to pass completely (append a directory like `basic` to scope). Two directive families keep it that way: diff --git a/js/src/night/build_wasm_jit_runner.py b/js/src/night/build_wasm_jit_runner.py index b012b632ab14e..e877343a11b4c 100644 --- a/js/src/night/build_wasm_jit_runner.py +++ b/js/src/night/build_wasm_jit_runner.py @@ -45,4 +45,13 @@ def main(output): os.replace(tmp, dst) installed.append(dst) + # The jit_test.py / jstests.py shim runs the shell under the runner; a copy + # next to the shell lets the harnesses find the objdir from its path. + shim = os.path.join(srcdir, "inproc-shell.sh") + dst = os.path.join(dist_bin, "inproc-shell.sh") + tmp = dst + ".tmp" + shutil.copy2(shim, tmp) + os.replace(tmp, dst) + installed.append(dst) + output.write("".join(p + "\n" for p in installed)) diff --git a/js/src/night/inproc-shell.sh b/js/src/night/inproc-shell.sh index 6bb9440a0fed0..3645142967878 100755 --- a/js/src/night/inproc-shell.sh +++ b/js/src/night/inproc-shell.sh @@ -11,7 +11,13 @@ set -u here=$(cd "$(dirname "$0")" && pwd) repo=$(cd "$here/../../.." && pwd) -SHELL_WASM=${NIGHT_WASM_SHELL:-$repo/obj-nightmonkey-inprocess/dist/bin/js} +# The build installs a copy of this script into dist/bin next to the shell. +if [ -f "$here/js" ]; then + default_shell=$here/js +else + default_shell=$repo/obj-nightmonkey-inprocess/dist/bin/js +fi +SHELL_WASM=${NIGHT_WASM_SHELL:-$default_shell} # Prefer the build-installed runner next to the shell (js/src/night/moz.build # installs it into dist/bin); fall back to the manual in-crate build. default_runner=$(dirname "$SHELL_WASM")/wasm-jit-runner diff --git a/js/src/tests/jstests.list b/js/src/tests/jstests.list index 2940baa1bbcb5..8c572740b07c4 100644 --- a/js/src/tests/jstests.list +++ b/js/src/tests/jstests.list @@ -47,15 +47,129 @@ skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/ skip-if(!this.hasOwnProperty("Intl")) script test262/language/literals/regexp/u-case-mapping.js # Unicode property escapes need the Intl property tables. -skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) include test262/built-ins/RegExp/property-escapes/generated/jstests.list +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/property-escapes/character-class.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/property-escapes/special-property-value-Script_Extensions-Unknown.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/prototype/exec/regexp-builtin-exec-v-u-flag.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-lower-p.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-upper-p.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/remove-ignoreCase-affects-slash-lower-p.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/remove-ignoreCase-affects-slash-upper-p.js skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/match/regexp-prototype-match-v-u-flag.js skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/replace/regexp-prototype-replace-v-u-flag.js skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/search/regexp-prototype-search-v-flag.js skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/String/prototype/search/regexp-prototype-search-v-u-flag.js - -# The wasi shell's stack is too small for 32 nested function literals. +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-difference-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-escape-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-escape-difference-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-escape-intersection-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-escape-intersection-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-escape-union-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-escape-union-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-intersection-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-intersection-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-union-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-class-union-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-difference-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-intersection-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-intersection-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-difference-character-class-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-difference-character-class.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-difference-character.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-difference-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-difference-string-literal.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-intersection-character-class-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-intersection-character-class.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-intersection-character.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-intersection-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-intersection-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-intersection-string-literal.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-union-character-class-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-union-character-class.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-union-character.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-union-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-union-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-property-escape-union-string-literal.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-union-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/character-union-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-character-class-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-character-class.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-character.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-difference-string-literal.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-intersection-character-class-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-intersection-character-class.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-intersection-character.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-intersection-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-intersection-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-intersection-string-literal.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-union-character-class-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-union-character-class.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-union-character.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-union-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-union-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/property-of-strings-escape-union-string-literal.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/rgi-emoji-13.1.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/rgi-emoji-14.0.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/rgi-emoji-15.0.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/rgi-emoji-15.1.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/rgi-emoji-16.0.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/string-literal-difference-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/string-literal-difference-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/string-literal-intersection-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/string-literal-intersection-property-of-strings-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/string-literal-union-character-property-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicodeSets/generated/string-literal-union-property-of-strings-escape.js + +# Case-insensitive matching under the u and v flags needs the Intl case +# folding tables. +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/unicode_full_case_folding.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-lower-b.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-upper-b.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-lower-w.js +skip-if(!this.hasOwnProperty("Intl")) script test262/built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-upper-w.js +skip-if(!this.hasOwnProperty("Intl")) script test262/staging/sm/RegExp/ignoreCase-non-latin1-to-latin1.js +skip-if(!this.hasOwnProperty("Intl")) script test262/staging/sm/RegExp/unicode-class-ignoreCase.js +skip-if(!this.hasOwnProperty("Intl")) script test262/staging/sm/RegExp/unicode-ignoreCase-ascii.js +skip-if(!this.hasOwnProperty("Intl")) script test262/staging/sm/RegExp/unicode-ignoreCase-escape.js +skip-if(!this.hasOwnProperty("Intl")) script test262/staging/sm/RegExp/unicode-ignoreCase-word-boundary.js +skip-if(!this.hasOwnProperty("Intl")) script test262/staging/sm/RegExp/unicode-ignoreCase.js + +# The wasi shell's stack is too small for these. skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script test262/language/statements/function/S13.2.1_A1_T1.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/RegExp/regress-119909.js + +# The wasi shell has no threads, so timeout() cannot create its watchdog. +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/extensions/regress-477187.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-452498-052-a.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-452498-114-a.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-479430-01.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-479430-02.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-479430-03.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-479430-04.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-479430-05.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/regress/regress-620376-1.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script test262/staging/sm/regress/regress-642247.js + +# The wasi shell has no time zone database and keeps UTC regardless of TZ. +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/Date/parse-dashed-numeric-date.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/Date/parse-period.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/Date/time-zone-2038-pst.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/Date/time-zone-pst.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/Date/time-zones-posix.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/Date/time-zones.js + +# The wasi shell has no os.system and no file-mapped ArrayBuffers. +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script shell/os.js +skip-if(this.hasOwnProperty("getBuildConfiguration")&&getBuildConfiguration("wasi")) script non262/extensions/file-mapped-arraybuffers.js + +skip-if(!this.hasOwnProperty("SharedArrayBuffer")) script test262/staging/sm/Atomics/cross-compartment.js +skip-if(!this.hasOwnProperty("SharedArrayBuffer")) script test262/staging/sm/Atomics/detached-buffers.js # The NightMonkey tier has no frame introspection: fun.caller and # fun.arguments read null from a compiled frame. From d38277b824d8152cbbbb82d7de89252ace5e85d0 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Sat, 5 Sep 2026 12:36:27 -0700 Subject: [PATCH 4/7] Design doc rewrite. --- js/src/night/docs/DESIGN.md | 4454 +++-------------------------------- 1 file changed, 388 insertions(+), 4066 deletions(-) diff --git a/js/src/night/docs/DESIGN.md b/js/src/night/docs/DESIGN.md index dde755f849166..316d6d6c54daa 100644 --- a/js/src/night/docs/DESIGN.md +++ b/js/src/night/docs/DESIGN.md @@ -1,4069 +1,391 @@ -# NightMonkey: design of the AOT JavaScript-to-WebAssembly tier - -**Status:** design of record. - -**Audience:** a compiler engineer joining the project, or a reviewer -evaluating it for upstreaming. It assumes you know compilers and WebAssembly -and nothing about this codebase. Where a mechanism is intricate, the document -says so and explains it rather than summarising it away. - -**Authority:** the source. Every claim here was checked against the code, and -the document cites `file:function` or `file:line` rather than other prose. -Where the source's own comments contradict the code, the code wins and the -discrepancy is called out. - ---- - -## Table of contents - -- [1. What this is, in one page](#1-what-this-is-in-one-page) -- [2. The soundness model](#2-the-soundness-model) - - [2.1 The contract](#21-the-contract) - - [2.2 The object stamp](#22-the-object-stamp) - - [2.3 What each stamp bit licenses](#23-what-each-stamp-bit-licenses) - - [2.4 Non-stamp discharge mechanisms](#24-non-stamp-discharge-mechanisms) - - [2.5 Failure modes, ranked](#25-failure-modes-ranked) -- [3. Repository map](#3-repository-map) -- [4. The code-emission strategy: workqueue BBV](#4-the-code-emission-strategy-workqueue-bbv) - - [4.1 What a version is](#41-what-a-version-is) - - [4.2 The context](#42-the-context) - - [4.3 The version table and the interning discipline](#43-the-version-table-and-the-interning-discipline) - - [4.4 The driver](#44-the-driver) - - [4.5 The continuation contract](#45-the-continuation-contract) - - [4.6 theta: the single merge point](#46-theta-the-single-merge-point) - - [4.7 Tracks: what replaced the OPT/OVF/GEN attractors](#47-tracks-what-replaced-the-optovfgen-attractors) - - [4.8 Loops: headers, back edges, side entries, on-ramps](#48-loops-headers-back-edges-side-entries-on-ramps) - - [4.9 Reducibility by construction](#49-reducibility-by-construction) - - [4.10 Carriers: values that cross versions unboxed](#410-carriers-values-that-cross-versions-unboxed) - - [4.11 Effects and LICM](#411-effects-and-licm) - - [4.12 Capacity: the compile ladder](#412-capacity-the-compile-ladder) - - [4.13 The program-point graph](#413-the-program-point-graph) -- [5. Lowerings](#5-lowerings) - - [5.1 Value representation and the cost primitives](#51-value-representation-and-the-cost-primitives) - - [5.2 GetProp](#52-getprop) - - [5.3 SetProp](#53-setprop) - - [5.4 GetElem and SetElem](#54-getelem-and-setelem) - - [5.5 Call](#55-call) - - [5.6 Construct](#56-construct) - - [5.7 The inline splice](#57-the-inline-splice) - - [5.8 Binary arithmetic](#58-binary-arithmetic) - - [5.9 Comparisons](#59-comparisons) - - [5.10 ToBoolean](#510-toboolean) -- [6. The analysis](#6-the-analysis) -- [7. opsem: the shared vocabulary](#7-opsem-the-shared-vocabulary) -- [8. The runtime and the ABI](#8-the-runtime-and-the-abi) -- [9. The two flows](#9-the-two-flows) -- [10. The regex compiler](#10-the-regex-compiler) -- [11. Limitations](#11-limitations) -- [12. Rough edges](#12-rough-edges) -- [13. Glossary](#13-glossary) - ---- - -## 1. What this is, in one page - -NightMonkey is an ahead-of-time compilation tier for JavaScript, built inside -SpiderMonkey, that emits WebAssembly. The name expands to *Nonlocal Inference -with Guiding Heuristics for Types*. - -The execution model is unusual and worth internalising before anything else: - -> **There is exactly one execution world and it is all-WebAssembly.** - -SpiderMonkey itself is compiled to `wasm32-wasi` (the *wasm shell*). The -compiler does not emit a separate module that imports the engine; it -**appends** compiled JS function bodies into the engine's own function index -space, table and linear memory. Consequently "call a runtime helper" and -"call another JS function" are both plain in-module wasm calls, and a compiled -body can read engine data structures with ordinary `i32.load`s at -statically-known offsets. Two wasm modules sharing one linear memory would -require a PIC dynamic-linking convention the engine build does not satisfy; -merging sidesteps that entirely. The backend is -[waffle](https://crates.io/crates/waffle) 0.3.1, whose lazy function bodies -let the ~9 MB engine module pass through as raw bytes while only the appended -functions are lifted to SSA IR. - -Each JS function is compiled to one wasm function that produces output -byte-identical to the interpreter, or is not compiled at all. Two flows share -one compiler (section 9): a **snapshot** flow (wizer snapshots the shell with -the program registered; the `nightmonkey` host binary rewrites the snapshot -with compiled bodies; stock `wasmtime` runs the result) and an **in-process** -flow (the shell compiles its own scripts and injects them into its running -instance through host calls; this is the jit-test vehicle). - -The compiler's inputs are **the program's bytecode and the snapshot's heap -state**, and nothing else. There is a standing design constraint behind this: - -> **No profiling input, ever.** The compiler may never require running the -> workload to collect a profile. Target programs may be runnable only in -> production, and there is no control plane that could feed profiles back; -> requiring one is not a requirement we can impose on a user of the compiler. -> The snapshot image is program *state*, not an execution trace, so reading it -> is allowed. Count-based or first-seen dynamic recording feeding compile-time -> decisions is not. Where static analysis and heuristics reach their limit, the -> answer is to stop, not to profile. - -Two pieces do the work: - -- **`compiler/src/likelier/`** — a speculative, context-sensitive, - whole-bundle type analysis producing *likely facts*: callee sets, instance - layouts, receiver classes, per-field numeric masks and value ranges. Nothing - it produces is trusted (section 6). -- **`compiler/src/wasm/bbv.rs`** — workqueue **basic-block versioning**, in two - passes over one body of code. `bbv/predict.rs` computes **one fact context - per program point**, to a fixpoint; emission then *consults* that prediction - and emits code enforcing it, shunting any execution that would diverge onto - the generic track. Reducible control flow comes by construction (section 4). - It is the only codegen path. - -The compiler has no design switches. `compiler/src/options.rs` carries exactly -two things: a leave-everything-interpreted triage switch, and a set of -diagnostics that change no generated code. The crate reads no environment -variables at all outside its own unit tests. There is one lowering strategy and -one analysis and they are not configurable. Inlining in particular has no -knob: its admission caps are constants in `wasm/bbv/` (section 5.7). ---- - -## 2. The soundness model - -This is the thing to check first, so it comes first. - -### 2.1 The contract - -> **Nothing the analysis produces is trusted. Every speculative fact is -> discharged at runtime by a guard, or is a sound static proof. A wrong -> prediction costs a failed guard or a generic helper call — never a -> miscompile.** - -Three corollaries follow, and all three are enforced structurally rather than -by review: - -1. **A script is compiled iff every reachable op in it is translatable.** There - is no partial compilation. `Outcome` has exactly two variants: `Compiled` and - `Skipped(reason)`. A skipped script keeps its AOT function index at 0 and - runs interpreted — a performance loss and nothing else. -2. **There is no deoptimisation and no bailout.** A failed guard is not an exit - from compiled code; it is a branch to a *different version of the same - successor pc*, compiled under weaker facts. There is no frame - reconstruction, no side-exit state map, and nothing to get wrong about them. - The cost of a miss is code duplication at compile time and a taken branch at - run time. -3. **A "fact" in the codegen's context is a proof, never a prediction.** The - analysis's predictions enter the codegen only as *arm ordering* and *guard - selection*. A guard that passes is what installs the corresponding fact into - the context. Manufacturing a type from a prediction is the one known - miscompile vector in this design, and the discipline against it is absolute. - -Where correctness rests on something other than a guard, it rests on a stated -invariant with a mechanical basis. There are three such invariants, and each is -called out where it applies: - -- **STAMP-IMPLIES-PLAIN-DATA** (section 2.2): a stamped object's guarded prefix - slots hold plain data properties, so a passing stamp test is itself the proof - that no accessor, proxy handler, or exotic lookup can be reached. -- **The interval algebra is a proof, not a prediction** (section 7): an interval - attached to a value traces to canonically-boxed producers, and is the basis - for eliding overflow checks. -- **The splice's exception rule** (section 5.7): a splice site is refused inside - any non-loop try-note range of the caller, and a splice target is refused if - it has non-loop try notes of its own, so "throw out of a splice" reduces - exactly to "throw out of the whole compiled body". - -### 2.2 The object stamp - -The stamp is the load-bearing runtime invariant of the whole system. It is what -lets a property read cost one `i64.load`. - -**Where it lives.** A single 32-bit word in the `JSObject` header, at byte -offset 4 on `wasm32` — the alignment padding SpiderMonkey already reserved -between the shape pointer and `slots_`: - -``` -wasm32 JSObject: shape @0 | STAMP @4 | slots_ @8 | elements_ @12 | fixed slots @16... -``` - -Two consequences worth stating plainly. The mechanism is **32-bit only by -construction**: on a 64-bit build the padding word does not exist and every -maintenance call site compiles away. And the word is **zeroed by -`JSObject::initShape`**, so "unstamped" is the default state of the entire -heap and 0 can never be mistaken for a class. - -**Layout.** - -``` - bit 31 30 29 .......... 18 17 16 15 .............. 0 - +--------+--------+-----------------+------+------+------------------+ - |CONSTR- | RANGES | early key |SLOTS |TYPES | layout key + 1 | - | UCTING | | (while CONSTR.) | | | 0 = unstamped | - +--------+--------+-----------------+------+------+------------------+ -``` - -The low half is the **identity**: `layout_key + 1`, where the key numbers a -class the analysis discovered. The `+1` keeps 0 meaning "unstamped". Keys are -assigned **region-contiguously** by the analysis so that a whole family of -related classes is a single unsigned range compare in the emitted code. - -The high half carries three independent **validity bits** plus a construction -sentinel. While the sentinel is set the object is mid-construction, its identity -is not yet published, and bits 18..29 hold an *early key* naming the class it -is being built as. No identity-guarded arm can hit such an object, because the -low half reads 0. - -RANGES sits at bit 30, at the top of the early-key region rather than beside -SLOTS, so that the engine's unconditional clear of that bit can never corrupt a -key mid-construction. The key gave up its thirteenth bit for it. - -### 2.3 What each stamp bit licenses - -| bit | claims | set by | cleared by | licenses | -|---|---|---|---|---| -| **identity** (low 16) | this object is an instance of layout key *k* | the constructor-exit stamp; an init delegate's restamp (prefix key to full key); the allocation site's early-key seed while constructing | the wholesale clear (below) | every class-fact arm's key compare | -| **TYPES** | every *masked* field of this layout holds a value of its mask | seeded at allocation; carried forward by the exit stamp | the engine's store check, on any non-number value store; the wholesale clear | skipping the value tag test on a typed field load | -| **SLOTS** | the static slot predictions are valid for this object | seeded at allocation, **only from a keyed stamp** — a keyless sentinel never seeds it, because adds are uncheckable without a layout | the add check, when a property lands at an offset that contradicts the prediction; the wholesale clear | the baked `FIXED_SLOTS_BASE + 8*slot` immediate in every checkless load and store | -| **RANGES** | the predicted value *ranges* of this layout's masked fields hold | seeded at allocation; carried forward by the exit stamp | **unconditionally** by the engine's store check, number or not; by a compiled store whose value is outside the claimed window; the wholesale clear | a loaded value arriving with a proven interval, which then elides downstream overflow checks | - -The engine-wide claim TYPES makes is **numberness and nothing finer**. The -per-field mask survives only because every consumer re-checks it at the load -through a number-tag dispatch. RANGES cannot ride that discipline, because it is -consumed *checklessly* — nothing re-derives a magnitude at the load — which is -exactly why it needs a bit of its own that every unchecked write drops. - -Note the asymmetry between TYPES and SLOTS: **a value store can never clear -SLOTS**, because a value store cannot move a slot. That is what makes the -checkless load arm (section 5.2, L1a) safe once SLOTS has been proven in a -lineage. - -#### Who clears - -There are three mechanisms, at three granularities. - -**1. The store check** — `nightStoreCheckOrClear`, reached from -`NativeObject::checkStoredValue`, the funnel for all thirteen engine-side slot -store flavours (and, conservatively, for dense element stores too). It -unconditionally drops RANGES, then drops TYPES if the stored value is not a -number. It is a test-then-write: an unstamped receiver, the common case, costs -one load and never dirties the header cache line. - -Compiled code emits its own twin of this check inline (`emit_store_choke`, -section 5.3), specialised by what the site statically knows. - -**2. The add check** — `NightAddPropCheck`, called from both branches of -`NativeObject::addProperty`, from the slot-adding path, and from the compiled -add-transition replay. It fast-outs when SLOTS is already clear (one load on -every unstamped receiver), resolves the layout from the early key or the -identity, fast-outs when the new slot is past the guarded prefix bound, and -otherwise clears SLOTS unless the assigned slot exactly matches a prediction — -either this layout's, or that of a layout which strictly *extends* it and -predicts this atom at this slot, which is what lets a two-phase constructor -fill its prefix and then its suffix without losing the bit. - -**3. The wholesale clear** — `clearNightLikelyClass()`, which zeroes the word -outright. Six engine call sites, all of them structural departures from -plain-data-ness: - -| path | why | -|---|---| -| `NativeObject::toDictionaryMode` | slot assignments are no longer positional | -| `NativeObject::changeProperty` | includes data becoming an accessor | -| `NativeObject::changeCustomDataPropAttributes` | custom data property | -| `NativeObject::removeProperty` | `delete` | -| `NativeObject::freezeOrSealProperties` | attributes change | -| `JSObject::swap` | swapped guts may no longer conform | - -#### The STAMP-IMPLIES-PLAIN-DATA invariant - -There is deliberately **no refusal allowlist** in the runtime. The invariant is -maintained structurally, by four facts that together mean a stamped object's -guarded prefix cannot hold anything but plain data: - -1. **Birth.** The only paths that write a nonzero stamp are the AOT allocation - helper, which ends in a plain-object allocation, and `create_this` for - scripted non-derived constructors, which is also plain. Proxies, typed - arrays and every exotic class are unreachable from both. -2. **Default.** `initShape` zeroes the word on every other allocation in the - engine. -3. **Departure clears.** Every transition away from plain-data-ness — dictionary - mode, data-to-accessor, delete, freeze, seal, guts swap — clears the word - wholesale. -4. **Adds are checked.** An accessor or otherwise unpredicted property landing - *inside* the guarded prefix clears SLOTS. - -A getter or setter therefore cannot be reached by any stamp-guarded arm, and the -generic path is the only path that can reach an accessor. Two soft spots are -worth stating rather than leaving implicit: an accessor added *past* the -extension bound keeps both bits (which is fine, because the claim only ever -covers prefix slots — but that is a reasoning step, not a check); and -**prototype mutation is not hooked at all**, which is sound only because every -stamp claim is about *own* fixed slots. - -#### How a stamp compare discharges a fact - -One load, one mask, one compare: - -```wasm -i32.load $obj offset=4 -i32.const 0xFFFF | TYPES | SLOTS ;; the bits this consumer needs -i32.and -i32.const k | TYPES | SLOTS -i32.eq -``` - -A single compare proves both "the identity is *k*" and "the required validity -bits are live". Variants pick exactly the bit set the consumer needs; a -range-valued identity fact becomes an unsigned-subtract range test instead of an -equality; and a consumer that needs only the flags loads the 16-bit half at -offset 6 so it can test them without touching the identity. - -That is the whole discharge mechanism. `refine_src` then records the proven -facts against the receiver's frame slot, so the next access on the same value in -the same lineage needs no test at all. - -#### The compiled-side validator - -The compiler does not trust the analysis's slot predictions either. Before a -constructor's exit stamp publishes an identity, emitted code loads the shape's -small slot-span field and requires it to be at least the predicted row length. -The soundness argument is that the predicted prefix is a name-to-slot bijection -and the engine assigns added slots sequentially, so a surviving SLOTS bit plus a -count of at least *N* implies the first *N* slots hold the predicted names. - -The add-check bound is a static table, stored per layout. - -### 2.4 Non-stamp discharge mechanisms - -Not everything is a stamp. The other discharge mechanisms, each with what makes -it sound: - -| mechanism | discharges | soundness basis | -|---|---|---| -| tag tests | every primitive-type claim | direct test of the boxed representation | -| shape compares (inline caches) | a property's location on this exact shape | a shape is an immutable descriptor; the cache regions are zeroed on major GC, and cached pointers are tenured-only | -| callee identity compares (call cells, builtin cells, native pointers) | "this call site's callee is *X*" | value identity is object identity; cells cache tenured values only and are GC-zeroed; a monkeypatched builtin is a different value and self-misses | -| value fuses | "this global binding still holds the value we baked" | the fuse word *is* the guard; it is blown by any write through the engine, and is distrusted wholesale whenever interpreted code can write globals | -| the interval algebra | integer overflow, `-0`, and tag claims | a sound abstract-interpretation proof over exactly-representable integers (section 7) | -| the effect-provenance return word | "the callee did not disturb my facts" | the callee computes it; the caller tests it (section 5.5) | -| the clean-miss bit | "this cache miss ran no user code and moved nothing" | the runtime helper returns it, and returns it only from arms that provably qualify | - -### 2.5 Failure modes, ranked - -The design's whole cost structure is this ladder, cheapest first: - -| failure | cost | -|---|---| -| a check the interval algebra elided | *does not exist* — no code is emitted | -| an overflow or `-0` exit | a convert and a reinterpret, then an edge to the `Side` version of the same pc. No spill, no call | -| a type-test arm miss | a few tag tests, then an edge. No spill, no call | -| an inline cache miss served cleanly | a spill, a helper call, a reload — but the lineage **rejoins its happy path**, facts intact | -| an inline cache miss that could have run user code | the same, and the lineage goes to the `Dirty` track: every class fact in the frame is killed and every GC-pointer carrier is swept | -| a generic helper arm | spill, call, reload, `Dirty` | -| a script the compiler declined | interpreted, at interpreter speed | - -Nothing below that line exists. There is no state in which a wrong prediction -produces a wrong value. ---- - -## 3. Repository map - -Everything lives under `js/src/night/`. - -| Path | Contents | -|---|---| -| `compiler/` | The Rust compiler crate, `night-compiler`. | -| `compiler/night-compiler.h` | The C ABI between SpiderMonkey and the crate. | -| `compiler/src/lib.rs` | FFI entry points (`night_inproc_build` and friends). | -| `compiler/src/options.rs` | `Options` / `Diagnostics`: the entire configuration surface. | -| `compiler/src/source.rs`, `src/source/ffi.rs` | The `Source` object graph — the sole input to the compiler. | -| `compiler/src/bytecode.rs` | Bytecode parser and `OpcodeVisitor`; `JSOp` is generated from `Opcodes.h` by `build.rs`. | -| `compiler/src/opsem.rs` | The shared op-semantics vocabulary: primitive-bit alphabet, result-type algebra, interval algebra. | -| `compiler/src/likelier/` | The speculative whole-bundle analysis (`scan`/`heap`/`calls`/`engine`/`types`/`emit`/`dump`). | -| `compiler/src/facts.rs` | `LikelyFacts`: the analysis-to-codegen contract. | -| `compiler/src/wasm/bbv.rs` | The workqueue-BBV codegen: versioning, all op lowerings, splicing, LICM. ~27k lines. | -| `compiler/src/wasm/bbv/predict.rs` | The prediction: one fact context per program point, and the fixpoint that computes it. The only fact store emitted code reads. | -| `compiler/src/wasm/translate.rs` | Shared substrate: `Helpers`, `AtomTable`, `Outcome`, layout constants, scan utilities, the entry shim into `bbv.rs`. | -| `compiler/src/wasm/effects.rs` | The effect taxonomy (`EffectClass`, `HeapKind`) used by call handling and LICM. | -| `compiler/src/wasm/regex.rs` | The regex AOT compiler (irregexp bytecode to wasm matchers). | -| `compiler/src/wasm/mod.rs` | `layout_env` / `translate_all`: analysis prepass, reserved memory-region layout, per-body translation, table and address patching. | -| `compiler/src/wasm/inprocess.rs` | In-process batch builder: function blobs plus the environment descriptor for the runner host calls. | -| `runtime/` | The C++ night runtime: the `night_runtime_*` helper ABI shim (`NightRuntime.cpp`) over the engine halves -- bytecode ops (`NightOps.cpp`), property caches (`NightInlineCaches.cpp`), inline allocation and barriers (`NightInlineHeap.cpp`), generators (`NightGenerator.cpp`), regexes (`NightRegExp.cpp`) -- plus entry into compiled bodies (`NightEntry.cpp`), the value stack, and snapshot registration and activation. Linked into the shell. | -| `snapshot/` | The snapshot / live-heap reader crate: parses the registration block and walks the script graph into a `Source`. | -| `snapshot-dump/` | A standalone wasmtime host tool that dumps a snapshot's `Source` graph, as a cross-check. | -| `nightmonkey/` | The `nightmonkey` host binary: wizer snapshot in, AOT-compiled snapshot out. | -| `wasm-jit-runner/` | Wasmtime-based test host exposing the function-injection host calls. | -| `tools/` | Diagnostic post-processors for the compiler's dump modes (`viz.py`, `opprof.py`, ...). | - -The waffle backend is an ordinary crates.io dependency (`waffle = "0.3.1"`), vendored at `third_party/rust/waffle`. It is not NightMonkey code, but two of its passes are load-bearing here and are described in [section 4.9](#49-reducibility-by-construction). ---- - -## 4. The code-emission strategy: workqueue BBV - -`compiler/src/wasm/bbv.rs`. This is the only codegen path. The design is -basic-block versioning in the Chevalier-Boisvert sense, driven off a -workqueue rather than a plan: the compiler processes one *version* at a time, -emits exactly one bytecode op for it, and routes every successor edge through -a single merge point. - -There are no ahead-of-time plans, no admission passes, no deoptimisation -landings, and no bailout mechanism. A failed speculation is an ordinary -control-flow edge to the generic version of the same successor pc. - -**One distinction runs through all of section 4 and is worth fixing before -reading it.** "Version" has historically meant two things; they are separate -objects here: - -- a **block** — `Ver { pc, class, track, depth }`, structural identity. The - token class and depth exist to keep every cycle single-entry (4.9), so two - blocks may be duplicates of one another for reducibility's sake alone; -- a **prediction** — the fact context, keyed by the program point and nothing - else. One per pc on the Opt track; the generic track carries no facts. - -Codegen never mints or moves a prediction. It reads one and enforces it. - -### 4.1 What a version is - -```rust -// bbv/version.rs -struct Ver { - pc: u32, // unified pc space: root bytecode + synthetic inline segments - class: u32, // interned token-vector id: the per-loop layer markers - track: Track, // Opt | Side | Dirty - depth: u16, // operand-stack depth at entry -} -struct VerId(u32); // bbv/version.rs -``` - -**The version identity does not mention the abstract context.** That is the -central design decision, and it is worth dwelling on, because the natural -formulation of BBV — a version *is* a `(pc, ctx)` pair — is what this replaced. - -Under `(pc, ctx)` identity, the context that a version carries is "whichever -lineage got there first", and every merge policy question ("may this edge join -that version? how many versions may a pc have? when do we widen?") becomes a -heuristic in the merge function. Under structural identity there is no merge -policy at all, because the two things have been separated: - -- `Ver` names a **block**. `class` and `depth` are there to keep every cycle - single-entry ([section 4.9](#49-reducibility-by-construction)); two `Ver`s - may be duplicates of one another for that reason alone. -- The **fact context** is keyed by the program point and by nothing else — - one prediction per `pc` on `Opt`, and none on `Dirty`, which carries no - facts ([section 4.2](#42-the-context)). - -Three consequences fall out: - -- token vectors naming a version stay stable across prediction rounds, so - version identity does not move under the analysis; -- **version count is bounded by construction** at two tracks per - `(pc, token class, depth)`, which retires every minting budget; and -- **every `Opt` block at a pc emits the same body**, because it reads the - same prediction. That is the test of the split: on an ISA that admitted - irreducible control flow the duplicate blocks would simply disappear, and - no dynamic path's code would change. - -`pc` is capped at 24 bits (`MAX_PC`, `bbv/mod.rs`); scripts over 128 KiB of -bytecode are not compiled at all, and inline splice segments allocate synthetic -pcs above the root script's bytecode in the same space (section 5.7). - -`depth` is normally a function of `pc`, since bytecode stack depth is -deterministic, and costs nothing. It is in the identity because an -**exception landing** enters a pc at the try-note's unwound depth — a second -legitimate depth for one pc (`bbv/version.rs`). It is *only* in the identity: -it keys no prediction, because a landing has no facts to misalign — every -landing edge goes to `Dirty` (`cont_stripped`), which is where a path that is -off every happy path by definition belongs, and also keeps a rare exceptional -state from pessimising the normal path it lands beside. - -### 4.2 The context - -```rust -// bbv/ctx.rs -struct Ctx { - locals: Vec, - stack: Vec, - args: Vec, // [0] = this, [1+i] = formal i - caller_locals: Vec, // the caller's frame facts across an inline splice - caller_args: Vec, - tokens: Vec<(u32, u64)>, // (loop index, layer marker), sorted - carried: Vec, // which locals arrive as block params - track: Track, -} -``` - -The context is a *split* object: `tokens`, `carried` and `track` are identity -or location dimensions, not facts. `tokens` and `track` are duplicated into -`Ver`; `carried` is pure location and is excluded from the implication -relation entirely. - -**Only the fact half is stored, and it is stored once per program point.** -`bbv/predict.rs` holds `Predictions`, whose whole content is: - -| keyed by | holds | -|---|---| -| `pc` | the fact context on the `Opt` track (`Ctx::facts_only`) | -| `pc` | the operand depth that prediction was minted at | -| `(pc, track)` | `carried`: which locals arrive as block params | - -Nothing else keys a prediction. Token class and depth name a *block*, not a -prediction. `Dirty` has no entry at all: **GEN carries no facts** — every -value is boxed at every op boundary there, and after out-of-lining -([section 5](#5-lowerings)) every GEN op body is a generic helper call that -would be handed a boxed operand regardless, so a fact there reaches no -lowering decision. `Ctx::facts_free` is its definition. - -A `Ver`'s emission context is therefore *derived*, never stored: the pc's -prediction, plus that identity's own tokens, track and carried set. - -`Predictions` is split down the middle on purpose. The **consult** half -(`at`, `carried`) is what codegen may use: it reads a prediction and emits -code that *enforces* it, shunting any execution that would diverge to GEN. It -never mints a fact and never moves one, and `theta` asserts it. The -**prediction** half (`join_arrival`, `set`, `widen_all`) belongs to the pass -that computes the fixpoint. - -Trailing all-TOP slots are trimmed by `Ctx::canon` (`bbv/ctx.rs`) and a slot -beyond the vector reads TOP, so contexts have a canonical form. - -The per-slot fact: - -```rust -// bbv/ctx.rs -struct SlotCtx { - prim_mask: u16, // PRIM_INT32 | PRIM_DOUBLE | PRIM_STRING | ... - outside: bool, // may be an object - range: RangeBucket, // I32 < I53 < Top - cls: Option<(u16,u16)>, // a proven stamped-class-key range - cls_shallow: bool, // the stamp's TYPES bit proven set - cls_slots: bool, // the stamp's SLOTS bit proven set - likely_cls: Option<(u16,u16)>, // ADVISORY class hint; invisible to `implies` - src: Option, // provenance: which frame slot this value IS - iv: Option<(i64,i64)>, // a proven exact-integer interval - iv_grow: u8, // fixpoint metadata: how often the interval widened - prov: Prov, // census provenance; excluded from Eq/Hash -} -``` - -Four fields deserve comment. - -**`likely_cls` is advisory and never proven.** It is the analysis's per-site -value class for the load that produced the value ("likely this class, -unchecked"). No consumer may trust it without emitting its own guard, which is -what promotes it to a proven `cls` on the hit arm. Because every use re-guards, -it needs no kill discipline — it survives a may-GC call and a restamp, where -`cls` dies — and it is invisible to `implies` and to the proof, because an -advisory fact owes no guard. - -**`prov` is census metadata, not a fact.** Its `PartialEq` is always true and -its `Hash` writes nothing, so it rides `SlotCtx`'s derived impls without -entering any comparison. - -**`src` is a location, not a claim.** In per-op BBV every operand a guard sees -arrived as a block parameter, so a guard that proves something about a value -has nowhere to write the proof back to unless it knows which frame slot the -value *is*. `src` is that pointer. It is excluded from `implies`, survives a -join only when both arrivals agree, and is consumed by `refine_src` -(`bbv/frame.rs`), which writes a guard-proven class fact back into the source -slot's context cell so later ops in the same lineage need no re-test. - -`refine_src` writes back **class facts only**. Numeric write-back was tried -and loses on most benchmarks, because re-typing a context slot hands out a -different carrier representation and pays a conversion on every subsequent -edge. The practical -consequence is worth stating plainly, because the surrounding code reads as if -it were otherwise: **a passed tag guard on a local does not make the next -arithmetic op on that local checkless.** Only class identity is durable across -ops in a lineage. - -**`iv_grow` is fixpoint metadata**, the widening trigger, and carries no claim. - -The implication relation (`SlotCtx::implies`, `bbv/ctx.rs`) is pointwise: -mask subset, objectness, range chain, class-key-range containment, and the two -stamp bits monotone, plus interval containment. `Ctx::implies` (`bbv/ctx.rs`) -is pointwise over all five slot vectors **with token vectors and track required -equal** — a token is an identity, not a fact. - -`SlotCtx::join` (`bbv/ctx.rs`) is the least upper bound: masks union, class -facts survive only if both sides claim, `src` survives only on agreement, and -the interval goes through `opsem::iv_join_tolerant`, which keeps the exact -union for the first three growths per slot and then snaps up a widening ladder -(section 7). - -### 4.3 The version table and the interning discipline - -```rust -// bbv/version.rs -struct VerTable { - ids: HashMap, - vers: Vec, // structural identity: a block - ctx: Vec>, // parallel, DERIVED: pred[pc] + this block's tokens/track/carried - pred: Predictions, // THE fact store, keyed by program point (bbv/predict.rs) - at_pc: HashMap>, // a prediction's dependents: the worklist's edge set -} -``` - -`VerTable::intern` is the only dedup point. The waffle side is three parallel -maps on the emitter: `blocks: HashMap`, -`block_params: HashMap>`, and the workqueue itself. - -Contexts are **never hashed**. The compile-time budget for a large program -does not survive hashing free-form contexts on every edge, so the only thing -interned is the *token vector*: - -```rust -// bbv/version.rs -fn tok_class(&mut self, toks: &[(u32, u64)]) -> u32 -``` - -backed by `tok_classes: HashMap, u32>`, which **persists across -prediction rounds** — a class id is part of a version identity, so it must name -the same thing in round N+1 that it named in round N. Both the version table -and the token-class table are moved forward between rounds rather than rebuilt. - -**The splice set persists for the same reason, and it is the sharper case.** A -spliced callee body owns a range of synthetic pc space, and the prediction is -keyed by pc — so a later walk that renumbered the segments would silently -re-point every prediction past the first splice. The splice set is therefore -decided on the first walk and then **frozen** (`Splices`, -`Bbv::adopt_splices`, `bbv/mod.rs`): later walks may only look segments up, -and a site that finds none lowers generically. - -Frozen means *decided*. The admission questions -- the per-script site cap, -the args-object bar, the depth cap, the target rejects, the handler ranges -- -are answered on the walk that created the segments and must not be asked -again, because the freeze hands the next walk their budgets already spent. -Re-asking them declines a splice walk 1 made, and the artifact then emits real -calls through a pc range the prediction still describes as spliced. That is -not a size decision, it is the two halves of the compiler disagreeing about -the program. The splice **fuel** is the one -gate that is not admission -- it asks whether this body has grown enough to -stop expanding it, which is a property of the walk doing the emitting -- so it -alone is re-asked, and both lowerings are correct at a spliced site. - -Each segment's loop intervals -are re-appended in segment order, so a loop's index — and hence every interned -token class — names the same loop in every walk. Attempting instead to gate -splices *during* emission corrupts exactly this numbering, reproducing as a -crash once splice admission depends on a changed walk. - -`Ctx` derives `Hash`, but nothing uses it; that derive is vestigial. - -### 4.4 The driver - -`translate_script` (`bbv/mod.rs`) is the outer harness. Up-front refusals: -bytecode over 128 KiB, generator/async bodies that use `arguments`, and -unsupported environment ops (section 11). Then, per compile-ladder rung (section 4.12), a **two-phase -build**: - -``` -predict::run(...) // phase 1: the prediction (bbv/predict.rs) -t = Bbv::new(...); // phase 2: mode = Code, CONSULT-ONLY -t.vers = vers; -outcome = t.emit(); -if t.map_changed { retry the closure, or descend the ladder } -``` - -and inside `predict::run`: - -``` -loop { - t = Bbv::new(...); t.mode = ContextOnly; - t.vers = vers; t.tok_classes = tok_classes; t.adopt_splices(splices); - t.emit()?; // walks the program, appends NO IR - vers = take(t.vers); ...; splices = t.take_splices(); - if !t.map_changed { break } - rounds += 1; - if rounds >= cap { vers.strip_all(); stripping = true } // loud safety net -} -``` - -The **same `emit_op` body serves both modes** (`EmitMode`, `bbv/ops.rs`). In -`ContextOnly` the IR primitives append nothing, but a virtual value counter is -bumped so that every size-sensitive decision (splice admission, the body-size -cap) sees the same number in both modes and takes the same control decisions. -That counter is charged **per identity**, so re-running a block replaces its -contribution rather than adding a second copy. This is the maintainability -property the whole design rests on: the abstract transfer function and the -lowering cannot drift apart, because they are the same code. - -**Inside one walk the fixpoint is a worklist over program points, not a -sequence of whole-program re-walks.** When `theta` moves the prediction at a -pc it re-arms exactly the blocks at that pc (`rearm_pc`), refreshing their -derived contexts as it does; those blocks re-run and propagate. There is no -dependency graph beyond that. The outer rounds remain only because a walk can -discover blocks the previous one never reached, and because the splice set is -frozen only after the first. Getting this wrong is expensive rather than -merely slow: before the worklist, keying the prediction by pc took raytrace -from ~10 rounds to 438 and regexp from 1.6 s to 132 s of compile time, -because a lattice step cost a whole-program walk. - -**Why a fixpoint first, rather than emitting and regenerating?** Because the -moment a back edge weakens a header's context, the preheader's already-emitted -edge targets the wrong version, and repairing that is not a retarget: the -context transition may need conversion code the edge never had, which cascades -to *its* predecessors. That cascade is a fixpoint, and it would be paid in -re-emission rather than in a cheap analysis walk. - -`emit()` (`bbv/version.rs`) sets up the frame and then drains the queue: - -```rust -while let Some(key) = self.workqueue.pop() { - if !self.processed.insert(key) { continue } - if self.value_count() > MAX_BODY_VALUES { return Ok(()) } // early abort - self.run_version(key)?; -} -``` - -The frame prologue lives in a synthetic entry block **outside the version -table**, so a branch back to pc 0 can never re-run it. - -`run_version` (`bbv/version.rs`) emits exactly one op: - -1. restore the emission point, the frame view (for splice segments), the - tokens and track, the four fact vectors, and the carrier caches; -2. rebuild the operand stack from the block's parameters, re-attaching each - slot's class facts, provenance and interval — dropping those made every - stack operand's proven class die at the next op, which in per-op BBV is - immediately; -3. decode one op at `pc` and lower it; -4. if the lowering did not set a terminator, add the fall-through edge through - `cont(next_pc)`. - -So **every op ends its block.** One waffle block per version, plus whatever -intra-lowering blocks the op's own diamonds needed. - -There is no scheduling pass. Blocks land in the waffle arena in creation -order, which is edge-discovery order; waffle recomputes RPO and the dominator -tree and emits in its own order (section 4.9). - -### 4.5 The continuation contract - -Every successor edge any lowering produces goes through one seam: - -```rust -// bbv/version.rs -fn cont(&mut self, succ_pc: u32) -> BlockTarget { - let normal = self.cont_normal(succ_pc); // computed FIRST, unconditionally - if let Some(t) = self.try_onramp(succ_pc, &normal) { return t; } - normal -} -``` - -The ordering is load-bearing: the normal continuation is the on-ramp's own -bail target, so the merge-point call sequence and every context join are -identical whether or not the on-ramp fires. The failure arm mints nothing. - -`cont_normal` (`bbv/version.rs`) builds the arrival context — the live fact -vectors, the operand stack projected slot-wise, the outgoing token vector, and -the carrier sets — canonicalises it, hands it to `theta`, and then calls -`cont_at` to materialise the edge. - -`cont_at` (`bbv/version.rs`) does the mechanical half: - -1. ensure the target version has a block (`ensure_version_block`); -2. build the argument vector in the exact parameter layout - `[ stack(depth) | carried locals | carried args | flags? ]`, converting each - operand to the target slot's representation via `convert_to_repr`; -3. in `Code` mode, assert that the argument count matches the target's - parameter count. Only `Code` asserts: the carried half comes from the - version context, which is frozen by the time `Code` runs but still moving - during the fixpoint rounds. - -`ensure_version_block` (`bbv/version.rs`) is where a continuation becomes a *new* -version: if the identity has no block yet, create one, type its parameters from -the version context, and push it on the workqueue. Otherwise reuse the existing -block and just supply edge arguments. So all tails are shared, and emitted code -is bounded by `O(|ops| x |versions per pc|)`. - -Two wrappers exist for readability: `edge_to` is `cont`, and `dirty_edge_to` -is also `cont` (the track already carries the dirt). `cont_stripped` -(`bbv/version.rs`) is the exception/finally landing form: it flushes deferred -state, boxes the whole operand stack, zeroes every fact vector, forces the -`Dirty` track, and then routes through the same seam — which is what keeps a -landing block's parameter layout in step with `run_version`. - -The invariants the contract enforces: - -1. every edge's arguments match the target's parameters in count and type; -2. every arrival's context **implies** the target version's context, so each - representation conversion on the edge is licensed; -3. stack depth agrees, because it is in the identity; -4. a version's block is created at most once. - -### 4.6 theta: the single merge point - -```rust -// bbv/version.rs -fn theta(&mut self, pc: u32, ctx_in: Ctx) -> VerId -``` - -`theta` is the only place a merge decision is made, and **it holds no policy.** -That is a change from the original design, which had three attractors and a -per-pc budget; section 4.7 explains what replaced them and why. What theta does -now, in order: - -1. **Ladder collapse.** On the `gen_only` rung, `ctx_in.gen_collapsed()` drops - every identity dimension but keeps the facts — one version per pc that still - hands out unboxed block parameters where all arrivals agree. Under - `stripping`, `ctx_in.stripped()` drops facts and carriers too. -2. **Peel fold.** A `Side`-track arrival at a pc inside a loop structure is - folded to `Dirty`: one peel per loop, not per track. -3. **Facts-free GEN.** A `Dirty` arrival is reduced to `ctx_in.facts_free()`. -4. **Identity interning.** Intern the token vector to a class id, then intern - `Ver { pc, class, track, depth }`. -5. **The prediction step, in the prediction pass only:** - -```rust -if track == Opt && mode == ContextOnly { - if vers.pred.join_arrival(pc, depth, ctx_in.facts_only()) { // pc-keyed! - map_changed = true; - self.rearm_pc(pc); // the worklist - } -} -``` - -`join_arrival` is the same lattice step as before — discovery, or -`join_iv_only` for an arrival that disagrees only on intervals, or the full -join — but keyed by the **program point** rather than by the block identity. - -6. **The derived context.** The identity's `ctx` becomes `pred[pc]` plus its - own tokens, track and carried set (or, on `Dirty`, no facts at all). - -In `Code` mode step 5 does not run at all: `theta` is a pure lookup, and a -`debug_assert` checks the contract the edge conversions rest on — that the -arrival implies its program point's prediction. Reaching a block the -prediction pass never saw is likewise a debug assertion failure; in production -both are caught by the closure check described below. - -The `implies_sans_iv` special case exists because an arrival that disagrees -*only* on the interval should weaken the interval in place rather than run a -full join — a full join would drop disagreeing `src` provenance, and thereby -change the code emitted, on account of a dimension no consumer had asked about. - -**Termination** rests on three independent arguments: - -- *The lattice is finite and joins only descend.* Masks are 16 bits, the range - bucket is a 3-chain, class ranges only widen, the stamp bits only clear, - `src` only goes to `None`. Intervals are the one unbounded-looking component - and are bounded by the widening ladder keyed on `iv_grow`. -- *Identities are finite.* `Ver` ranges over `pc x class x 3 tracks x depth`, - and a token vector has one entry per enclosing loop drawn from three markers. -- *There is a hard escape.* If the fixpoint exceeds `max(48, versions/4)` - rounds, `strip_all()` widens every context to its stripped form. That is - guaranteed to close: with every context stripped, a version's context is a - function of its own identity, so no arrival can move one and the next round - walks exactly the same versions. It prints a warning unconditionally, because - convergence is guaranteed and any firing is a bug to chase — the strip is a - safety net, never policy. - -The **closure check** after the `Code` pass is the containment property: if -emission discovered a version the fixpoint had not, the emitted body may have -converted an edge to a representation the value was never proven to have, so -the body is *not* emitted. It retries the fixpoint up to three times and -otherwise descends the compile ladder, whose bottom rung is facts-empty and -where the check cannot apply. - -### 4.7 Tracks: what replaced the OPT/OVF/GEN attractors - -The original design had three per-pc attractors — OPT (refining joins only, -per-pc budget K), OVF (numeric-degraded, absorbing), GEN (bottom). They are -gone: the code does not implement them, and there is no per-pc budget -anywhere in the source. - -What replaced them is a **structural dimension** in the version identity: - -```rust -// bbv/ctx.rs -enum Track { Opt, Side, Dirty } -impl Track { fn step(self, to: Track) -> Track { self.max(to) } } -``` - -`Opt` means exactly one thing: the execution **conforms to the prediction at -every program point it has passed**. `Dirty` is where a non-conforming -execution is shunted, and it carries no facts. - -**A call does not step the track.** A may-GC call kills the class facts, and -one might expect the call-crossed lineage to be kept separate so those weak -facts cannot join into the strong pre-call version (the join law). With the -fact context keyed by the program point there is no version to poison — a pc -has one prediction, which is the join of its own arrivals and nothing else — -so that separation buys nothing, and what stands in its place is the pressure -valve the design rests on: a value the analysis cannot pin after a call is -predicted weakly and stays generic *on Opt*, rather than routing the whole -call-heavy tail of every body into fully generic code. Opt does not mean -fast; it means conforming. - -**One contract follows from this, stated only in prose.** `dirty_edge_to` — -the continuation an IC-miss or slow-helper arm takes — steps the track -itself, scoped to the edge. It must: what such an arm delivers is a -*claim-free* result, and with one prediction per program point a single -untyped `Opt` arrival degrades every lineage through that point. On -navier-stokes that turns a 1-instruction `f64.mul` into a 410-instruction -ladder two bytecodes downstream of a slow element arm. This is the standing -rejoin rule applied literally: an arm that proves nothing is not conforming, -whether or not it contradicts anything. - -**What not stepping the track on a call buys**: substantially cheaper code on -call-heavy benchmarks and OPT residency in the high nineties percent across -most of the suite. What remains against it is that **the -lowerings answer to the track rather than to the prediction** (see -`outline_generic`, `bbv/outline.rs`): a weakly predicted op on `Opt` gets the -full speculative bundle, 410 emitted IR instructions for that navier `Mul` -against 15 for the same op out-of-lined on GEN and 1 with its operands typed. -The pressure valve describes a generic-on-`Opt` shape the emitter does not -have, and no policy patch on the *track* supplies it — conditioning the step -on whether the call destroys a class fact the lineage holds recovers nothing. - -- `Opt` — the happy path. An op's happy path needs no analysis to identify: it - is the arm that *falls through*. -- `Side` — reached through any `side_arm`: a missed guard, an overflow exit, an - inline slow arm. -- `Dirty` — GEN: a non-conforming execution, carrying no facts. - -Steps only ever descend, so the track is a sticky property of how control got -here, and **two tracks never join**. That single fact is what the attractor -policy was trying to achieve. The failure mode it fixes is concrete: under a -first-arrival policy, `+`'s slow arm — which pushes a bottom type because `+` -may concatenate — could *define* the successor pc for the int lineage that fell -through it. On one benchmark kernel, 63 of 434 merge decisions were such -demotions and all 11 fully-generic arithmetic sites sat on OPT versions. Under -the track split that slow arm is on `Side` and cannot reach the `Opt` version -at all. - -A `side_arm` (`bbv/arith.rs`) saves the entire lineage state (stack, all four -fact vectors, carrier caches, flags, track), steps the track down, emits the -arm, edges to the *same successor pc* under the arm's own weaker facts, then -restores. Every branch splits context; nothing merges back. - -There is one further collapse: theta folds `Side` into `Dirty` wherever a loop -header exists to rejoin at, because `Side` is nearly zero dynamically and one -peel per loop is cheaper than one per track. Straight-line side lineages, which -have no header to rejoin at, keep their own copies. - -`gen_only` — the bottom rung of the compile ladder — is the third thing the -old GEN attractor named: every version at every pc is facts-empty, giving back -exactly the generic lane's code. It is a compiler-capacity fallback, not a -runtime deoptimisation. - -### 4.8 Loops: headers, back edges, side entries, on-ramps - -Loop extents come from a purely syntactic scan (`translate.rs` -`scan_loop_intervals`): `LoopHead` pcs, and each negative-offset branch whose -target is a `LoopHead`. Callee loops are appended, base-offset, when a splice -segment is created. - -Each context carries a **token vector**: one `(loop index, marker)` entry per -enclosing loop, with three markers (`bbv/mod.rs`): - -| marker | meaning | -|---|---| -| `TOK_PEEL` | acyclic at this level — side entries, mid-body track steps, every distinct outer history, all funnelled into one class | -| `TOK_CYCLE` | steady dirty cycling; what a non-`Opt` header hands its body | -| `TOK_OPT` | the `Opt` cycle layer; what an `Opt` header hands its body | - -`out_tokens_for` (`bbv/version.rs`) computes the outgoing vector. Three rules -matter: - -- **The header-drop rule.** The containment test for loop `(h, e)` is - `h + 1 <= pc < e`, i.e. *strictly interior*. An edge to the header itself is - not "into the loop body", so the loop's own token is omitted from the vector. - Entry edge and back edge therefore produce the same token vector, hence the - same class, hence the same version. **This is what makes each cycle have - exactly one entry.** -- A header hands its *own* side's marker to its body, so every body version - carries a token naming which header it belongs to. -- A non-`Opt` lineage's marker collapses to `TOK_PEEL` unless it is the loop's - dirty-cycle membership. - -A **side entry** is an edge into a loop's strict interior from outside — it -takes `TOK_PEEL`, and the peel copy is acyclic by construction: its only way to -cycle is through a header, which re-marks it. - -**Header versions are not pinned or special-cased.** A header pc gets one -version per (token class, track, depth) reaching it, like any other pc. The -loop stays typed if and only if its body preserves the header's context; that -fixpoint emerges from the driver rather than being proven ahead of time. - -A loop header's *entry* and its *steady state* are one version. Splitting them -— peeling the first iteration so the entry context need not join the steady one -— was tried and loses overall: it helps the one benchmark it targets and costs -several others, because peeling every loop's entry costs a second copy of -every loop body. - -#### On-ramps - -An **on-ramp** is an edge-owned guard chain that lets a `Side` or `Dirty` -lineage re-enter the `Opt` version of a loop header by re-proving every fact -that header's prediction claims. This is the design's one load-bearing -asymmetry: the on-ramp edge **proves** the prediction instead of joining into -it. - -**Extending it to `Opt` sources does not work.** With the fact context keyed -by the program point, an `Opt` edge whose arrival is weaker than its target's -prediction no longer lands in a version of its own — it degrades the one -prediction every lineage through that point reads, which is the join law with -nowhere left to hide. Having such an edge prove the prediction instead (same -target block, a guard chain in front, a bail to the pc's GEN version) is the -conformance-by-construction answer, and Opt residency collapses under it -because the guards fail. It is the same population lesson as the dirty -cycle's own back edge below — an edge that reaches the proof every iteration -pays every iteration, and a proof that can fail will. - -`try_onramp` (`bbv/version.rs`) declines unless: the target pc is a loop header; -the current track is not already `Opt`; the source is either strictly inside -the loop (a *funnel*, requiring a `TOK_PEEL` label) or outside it (an *entry*); -every outer token label is `TOK_CYCLE`, since a `TOK_PEEL`-labelled tail would -rejoin the steady `Opt` header off its dominance region — the irreducible -shape; and **the header pc has an `Opt` prediction at all**. - -That last condition is not "the target `Opt` version exists" -- the -difference is the whole of what the prediction split buys here. A proof -copy of a header and the steady `Opt` cycle are two blocks at one pc, so they -read *one* prediction: there is no per-copy context to seed, no rule for which -existing version to copy it from, and no "unseeded" case to decline. What is -left is the original question — is there anything to rejoin — asked directly. - -**Header depth is not one of the conditions.** The proof target is keyed by -the arriving lineage's own outer labels with this loop's token dropped, so for -an inner header it is a *copy* of that loop rather than the steady `Opt` -version, and the copy cycles alone — reducibility by construction. The funnel -form does not additionally require the outermost level: that requirement -would be redundant, because every funnel it would turn away is already turned -away by the `TOK_CYCLE` rule above, which is where the reducibility argument -actually lives. - -Feasibility is then checked over **every slot the target context claims** — -the whole stack, all locals, `this` and every formal — because a fact on a -frame-resident slot is trusted at its next read and therefore owes a guard even -when nothing is carried. `conform_gap` (`bbv/version.rs`) answers, per slot: not -re-provable / already implied / provable with a guard. There are three guard -kinds: a **tag test** on a boxed source, an **exact-i64** round-trip check, and -an **exact-f64** round-trip check. - -The tag test proves membership in the target's whole admissible tag set -- -one compare per tag, OR-ed, with the int32/double pair collapsed to the single -`is_number_tag` range test wherever both are present. Proving only one tag at -a time (plus the numeric pair as a special case), with `conform_gap` refusing -any target it could not express that way, would throw out ordinary unions -like `int32 | object`; those are exactly what a dirty loop body carries, so -refusing them would decline the recovery edge of box2d's hottest loop. -Several compares is the right price: the alternative to a successful on-ramp -is the -whole remainder of the loop off the Opt track, and the alternative to a failed -one is the dirty continuation that already existed. - -`exact-f64` is `exact-i64`'s twin for an unboxed f64 carrier re-proving exact -int32 -- the widened-counter case. It is `f64.eq(x, convert_i32_s(trunc_sat(x)))` -**and** an explicit rejection of the `-0` bit pattern: unlike an integer -carrier, an f64 can hold a negative zero, which round-trips through 0 and -compares equal, and boxing it as int32 0 is a real change of value. - -Two claims still refuse a proof outright, and both are about what the guard -chain cannot reach: an identity claim (`cls`/`cls_shallow`/`cls_slots`) on a -target that is not object-only, because phase 2's class-word read is licensed -only behind phase 1's object proof; and a `range` claim on anything but a bare -int32 target, because the range rides on that tag and nothing else. - -**The caller's frame is a proof source, not a refusal.** A header inside a -spliced segment claims the caller's frame facts, because a splice carries them -into the segment ctx by construction. Refusing those claims outright, on the -reasoning that they are compile-time carried state the edge cannot re-prove, -would be a mistake — that reasoning is really an argument about facts the -*arrival* has, and an on-ramp's arrival never has any: GEN carries no facts, -so every -on-ramp source is factless by construction. That is the premise of the -mechanism, not an obstacle to it. The question is only whether the proof can -*test* for the claim, and it can: the frame is flat memory, the parent -segment's view addresses it, and `ProofSrc::CallerLocal` / `CallerArgSlot` -load the slot and guard it like any other. Soundness needs nothing new — a -splice cannot assign the caller's frame, so no write can intervene between the -guard and the use, and the kill discipline already sweeps `caller_locals_ctx`. -Measured on call-heavy code such as pdfjs, this recovers several points of -Opt residency at negligible size cost. - -Two policy rules were each measured into place: - -- **Entry-edge conforms must be free.** An edge coming from outside the loop - declines if any slot owes a guard at all; only the funnel (a tail already - inside the loop, where the guards are paid once per recovery rather than per - entry) pays for them. -- **Entry edges decline on interval widening.** A funnel joins its delivered - intervals into the header's prediction; an entry that would widen a stored - interval declines instead — unless the gap is *guardable*, in which case it - pays a bounds check against the stored interval and delivers it unchanged. - - That clause is on unconditionally: a program point has one prediction, the - join of its own arrivals, so an admission cannot reshape the map. A proof - edge *proves* that prediction rather than joining into it, so the cliff - that would come from admitting a guardable edge into the wrong version is - structurally impossible rather than empirically avoided. - -Emission is two phases in fresh blocks that touch no lineage state: an -AND-chain of tag and exact-i64 guards, then a second AND-chain of class-word -tests. The phases are separate because a class-word read must only run behind -the phase-1 object proofs — a wild in-bounds read of a non-object payload could -spuriously match. - -The steady dirty cycle's own back edge deliberately does **not** conform: a -population whose facts are genuinely false (an unstamped receiver, say) would -pay the guards every iteration and fail them every time. That was tried, and -measured a 91% conform-failure rate. - -For a loop nest, a non-outermost header's proof target is a *new copy* of the -inner loop labelled by the arriving lineage's outer labels. The copy cycles -alone, and its outer back edge drops the outer token at the outer header and so -re-enters the outer `Opt` cycle at *its* header: one loop level recovered per -iteration, reducibility preserved. - -#### The just-in-time on-ramp: conforming at a call return - -The proof is not a loop mechanism. What is loop-specific is the *policy* -around it — which target, which edge classes may attempt one, what an -unprovable interval means — and that policy is now separated from the -mechanism: `conform_gaps` produces the plan (which slots owe which guard, -what interval each delivers) and `emit_conform` emits the chain, and both are -shared. - -The second caller is a call return. A call site with a keep fork sends its -merge to GEN, because every arm reaching it failed the callee's runtime -intactness proof — but that proof is all-or-nothing about the whole heap, and -failing it is not the same as contradicting anything this lineage believes. -So the merge asks the narrower question instead: re-prove the successor pc's -own prediction, fact by fact, and take the `Opt` version if the guards pass. -The target is the version the lineage was continuing into anyway (same pc, -same token class, computed as if the track had not been stepped, same depth), -so nothing about the CFG changes; the bail is the GEN continuation that -already exists; the failure arm mints nothing. - -Its admission rule is the pair the loop-header refutation showed was missing — -what the proof costs (the guard chain's length) against what it recovers -(the bytecode span from the successor to the end of the innermost enclosing -loop, past which the loop's own header on-ramp is the recovery mechanism). - -**Measured: correct and inert.** The proof's take rate on richards is -3,878 of 3,878 — every fact held across a call return re-verifies, exactly as -the design predicts, because the write the callee reported was to some other -object. But that population is 0.05% of the bench's scripted-call departures: -the effect-flag fork's epoch keep arm already recovers the other 99.95%, -so there is no post-call dwell left at a call return on this corpus. The -mechanism is kept for the population that exists elsewhere, and must not be -cited as a measured win. - -### 4.9 Reducibility by construction - -The argument, in full: - -> `out_tokens_for` drops a loop's own token at its header pc, so every edge to -> header `h` — entry or back — targets `(h, outer class, track)`. Every pc in -> that header version's body carries a token naming *that* identity, so a body -> version's only predecessors are that header and its own body. Each cycle -> therefore has exactly one entry, at the header. Tracks only descend, so a body -> version on a lower track back-edges to the lower track's header — again an -> entry at a header, never into the middle of a cycle. Cross-lineage edges -> (guard misses, weakened joins) land in `TOK_PEEL` copies, which are acyclic by -> construction. - -`assert_reducible` (`bbv/licm.rs`) checks it after emission — a retreating -RPO edge whose target does not dominate its source. This is exactly waffle's -own test. - -**The argument has a precondition, and it is now stated.** Every step above -rests on the emitter knowing where the loop headers are, and it learns that -from the `LoopHead` markers: `scan_loop_intervals` records an interval only -for a back edge whose *target* carries one. A back edge to an unmarked target -therefore gets no interval, so no token, so no re-labeling — and the cycle can -acquire a second entry, which is exactly the irreducible shape. - -Measured over the whole benchmark corpus, the count is zero: SpiderMonkey -marks every loop header, so real bytecode always satisfies the precondition. -The counterexamples were four hand-written unit-test programs -whose loops had no `LoopHead` at all (they were not being compiled as loops -either, so they were not testing what their names claimed). - -The precondition is a gate: the driver refuses a -script with a back edge to an unmarked target up front, alongside the other -structural refusals, and an irreducible edge surviving `assert_reducible` -after that is a compiler bug — the body is refused (`Outcome::Skipped`, which -leaves the script on the interpreter and is always sound) rather than shipped -with a loop analysis that could not run. - -There is **no relooper in this tree.** Structured control flow is -reconstructed by two waffle passes: - -- `reducify.rs` runs first and unconditionally. It scans for a retreating edge - to a non-dominator in `O(E)` and, in the by-construction case, returns the - body borrowed and untouched. When it does fire, it makes the CFG reducible by - context-sensitive tail duplication, cloning blocks per "skipped-header - context". That pass carries an explicit exponential-blowup warning, which is - why the by-construction property matters: it is the difference between a - no-op scan and a potentially multi-megabyte duplication. -- `stackify.rs` then reconstructs `block`/`loop`/`if`/`br` from the reducible - CFG. It implements Ramsey's *Beyond Relooper* (ICFP 2022), iteratively rather - than recursively, and hard-errors on an irreducible edge — which is what - reducify exists to prevent. - -waffle's `localify.rs` afterwards lowers SSA values and block parameters to -wasm locals with explicit moves at branch sites. Block parameters are a -waffle-IR construct; wasm has none. - -### 4.10 Carriers: values that cross versions unboxed - -A value crosses a version boundary **in its proven representation, never -reboxed**: - -```rust -// bbv/ctx.rs -enum Repr { - Boxed, // i64: the NUNBOX32 JS::Value - I32, // raw int32, tag proven - F64, // raw double - Bool, // 0/1 - I64, // exact integer, |v| <= 2^53, never -0 - StrPtr, // raw JSString* - ObjPtr, // raw JSObject* -} -``` - -`slot_repr` (`bbv/version.rs`) is the entire policy — it maps a joined fact to a -representation, and `ensure_version_block` types the block parameters from it: - -``` -prim_mask == INT32 && !outside -> I32 -prim_mask == BOOLEAN && !outside -> Bool -numeric && range != Top -> I64 (exact integer track) -numeric -> F64 -prim_mask == STRING && !outside -> StrPtr -prim_mask == 0 && outside -> ObjPtr -otherwise -> Boxed -``` - -The block parameter layout is fixed: -`[ stack(depth) | carried locals (sorted) | carried args (sorted) | flags? ]`. - -**On GEN there are no carriers**, because there are no facts: `slot_repr` of a -facts-free slot is `Boxed` for every slot, so every crossing is boxed. Note -what that does *not* say. `Ctx::carried` — which locals ride the edge as block -params at all — is a location, not a fact, and is untouched: the JS operand -stack and the carried locals still flow through SSA block params on GEN, they -are simply all boxed `i64`. Keeping values in SSA dataflow rather than -load/storing the AOT frame at every opcode is real and free, and is not what -facts-free GEN gives up. - -**Locals-into-SSA.** `Ctx::carried` names the locals arriving as block -parameters. It is a *liveness* set, not a fact set: the proposed set on an edge -is every hot local for which the emitter currently holds a live SSA value. The -alternatives were all tried and all lose: - -- gating on a non-TOP *fact* — this misses the point, because the frame - traffic that matters is the *un-carried reads*, not the typed ones, so a - hot function ends up with far more boxed loads than carrying by liveness - gives it; -- proposing every local the script touches; -- re-offering only what a may-GC call swept. - -An edge load is paid on *every* edge, and with this many versions per pc that -beats the lazy frame read it replaces. - -**Write-through discipline.** A local store still writes the boxed value to -its frame slot, so the frame is always root-complete and the GC's view is -never stale. Reads and version edges use the carrier. At a may-GC point -carriers are **dropped, not reloaded**: raw numeric representations are immune, -`StrPtr`/`ObjPtr` die (the frame slot is the copy the GC updates), and a -`Boxed` carrier survives only if its tracked fact excludes GC things. Reloading -at the sweep point would not even typecheck as SSA, since a may-GC call is -often emitted inside a diamond arm and a value defined there does not dominate -the merge that follows. - -Edge conversions (`convert_to_repr`, `bbv/object.rs`) are one instruction each -in the common cases. Two of them carry measured history: a boxed-to-f64 edge -deliberately takes the *uniform* unbox rather than a fact-specialised shortcut -so that identical unboxes GVN together, and the unboxed-to-f64 path was a -box-then-immediately-unbox round trip until it was fixed (+5.0% on one -benchmark). - -### 4.11 Effects and LICM - -Every emitted instruction is annotated at emission time in a side table keyed -by waffle `Value`. Two vocabularies (`compiler/src/wasm/effects.rs`): - -`EffectClass` — what a callee can *do*: - -| class | meaning | -|---|---| -| `Pure` | wasm ops, frame traffic, validated-fence loads, class-word tests, tag tests; helpers that only read engine state | -| `Leaf` | provably runs no user code and no GC | -| `Alloc` | may GC, never runs user code | -| `Unknown` | can reach user code | - -Unlisted functions — compiled bodies, stubs — are `Unknown`, because a scripted -callee runs arbitrary JS. Where a helper's contract is ambiguous the -classification is biased down (Leaf-or-Alloc picks Alloc; Alloc-or-Unknown -picks Unknown). - -`HeapKind` — *where* an access lands, the abstract-location vocabulary for -LICM write summaries: `EngineTable`, `FuseCell`, `Shape`, `ClassWord`, -`ElementsHeader`, `Elements`, `Slot`, `StringData`, `AllocCursor`, `Fresh`, -`Unknown`. Two of these encode real reasoning rather than a partition: - -- `EngineTable` rows (IC ways, cached call targets, guard cells) are hoistable - even across may-GC arms, because every consumer re-verifies against live - object state — a stale row read can only *miss*, never yield a wrong value — - and their addresses are compile-time constants that a GC cannot relocate. -- `FuseCell` is the opposite: the cell **is** the soundness guard, so it is - never hoistable across a call. -- `Fresh` names stores into nursery memory this body claimed from the bump - cursor. Without a GC the cursor only moves up, so fresh bytes are disjoint - from every address that existed at loop entry, and a `Fresh` write can never - invalidate an invariant load. - -What a **call kills** (`note_call_eff`, `bbv/facts.rs`): a may-GC call kills -every class fact everywhere in the frame (including the caller-frame facts -carried across a splice) and sweeps every GC-pointer carrier. It does *not* -kill primitive masks, ranges or intervals — those are properties of the value, -and they move with the object — and it does not step the track -([section 4.7](#47-tracks-what-replaced-the-optovfgen-attractors)). The arm -that *ran* the call steps it, at its continuation (`dirty_edge_to`), because -what that arm hands the successor is a claim-free result. - -There is one important exemption, the **quiet alloc**: an `Alloc`-class helper -that may GC but provably writes no pre-existing user-visible heap (object and -array literal allocation, `create_this`, string building, closure creation). -A GC invalidates raw pointers only, so a quiet alloc sweeps carriers and -nothing else — no track step, no fact kill. - -The arm-scoped alternative — "a call only dirties a lineage that has already -left the happy path" — is decisively worse: narrowing the kill that way is -about *where* the lineage is, not about how much of it a call may reach, and -it costs far more than it saves. The conclusion that stands: **keeping facts -across a call needs a proof, not a weaker dirt rule.** That proof is the -effect-provenance return word described in section 5.5. - -**LICM** runs on the emitted waffle IR, after the body is built and only if -`assert_reducible` succeeded. It identifies natural loops from back edges whose -target dominates the source, processes them innermost-first so inner hoists can -cascade outward, and hoists a load when its address operands are loop-invariant -and no in-loop instruction's effect may-write the load's heap kind. The pass -early-outs in two RPO scans when there is no retreating edge at all, which is -the common straight-line case. - -### 4.11a Per-binding value facts - -A `GetGName` of a syntactic global whose name is an own data property of the -snapshot's global object (`EnvLayout::gcell_bids`) carries a **value fact**: -"binding B's value has tag type T", in `Ctx::gcells` beside the slot vectors -(joined pointwise, part of `implies`, in the per-pc prediction; frame- -independent, so one vector serves the root and its spliced segments). The -read's tag ladder installs it; a later read whose held fact implies the pc's -claim runs the fuse/slot diamond alone and pushes from the fact -(`SlotRef::GCell`). It is deliberately a fact about the value, not about the -cell: it holds on the fuse-hit AND the guarded-slot arm, so no fuse miss has -to leave Opt (the in-process lane distrusts every value fuse, and a `delete` -or `defineProperty` on the global blows a cell for good). For a fact-carrying -binding the read's generic-helper arm has no Opt keep continuation (it would -rejoin `next_pc` without the fact and strip it from the prediction). - -What kills it: any `SetGName`-family store in the body (the inline form -re-installs the stored value's fact for its own binding and keeps the -others; the generic form, which can run a setter, kills all). Across a call -the fact survives a keep continuation when the callee's word has `FLAG_BIND` -clear (bit 3 of the sig2 word, set by every inline and generic binding -store, carried by the fold masks) OR the **binding-write epoch** is unchanged -(`gBindEpoch`, bumped by the runtime's fuse blow/value-change paths and by the -compiled inline store; its address is published in the strlit block like the -stamp epoch's). Otherwise each fact is re-proven on the spot through the leaf -`night_runtime_binding_value` (armed cell, else guarded resolve and slot, -else a magic Value) and a tag test, failing to `dirty_edge_to`; on-ramp proofs -discharge the facts the same way (`ProofSrc::GCell`). Neither the stamp word -nor the stamp epoch can stand in: a binding is a slot of the global object, -not a claimed layout. - -### 4.12 Capacity: the compile ladder - -Version count is bounded structurally, but *size* is not — the wasm function -size limit and the backend's tail duplication are real and independent. So -there is a cap, `MAX_BODY_VALUES = 300_000` SSA values, and a three-rung -descent when it is exceeded: - -``` -rung 0: full refined -rung 1: splice-facts off (only taken when that dimension plausibly tipped - a near-cap script; huge overflows skip it) -rung 2: gen-only (facts-empty versions everywhere) - -> still over cap? Outcome::Skipped("body too large") -``` - -The same ladder catches a failed closure check. Its bottom rung always -terminates, because facts-empty versions cannot move under any arrival. - -A generator or async body starts on rung 2 and stays there. That is not a -capacity decision: a version's identity names a *location* inside the body -(its loop-token class and its track), and a suspend leaves the body -entirely, so a resume has no arriving version to name. On the gen-only rung -there is exactly one version per pc, which the resume dispatcher can name by -pc alone. See section 5's generator ladder and `bbv/generator.rs`. - -A rejected alternative is worth naming: a single global "mint fuel" counter -gating optimistic version minting, twin minting, dirty forks *and* splice -admission at once would let one cliff silently change four unrelated -decisions. The caps are per-decision instead. - -### 4.13 The program-point graph - -`bbv/cfg.rs` builds a CFG, dominator tree and loop nest over the **unified pc -space** — the root script's bytecode plus every frozen splice segment, in the -numbering `ensure_seg` laid out. It is the graph over *program points*, which -is the space the prediction is keyed by (4.1), and therefore the space a fact -question is asked in. It is distinct from both of the graphs that existed -before it: `likelier/` has no blocks at all, and the emitter's graph is -waffle's, which is one node per emitted **version**. - -It is built lazily, once per walk, and only after the splice set is frozen -(4.3) — before that a segment the next walk creates would extend a pc space -the graph does not cover. - -Three things need it. "Check here, trust below" is a dominance query, and it -is what replaces `refine_src` when the guard-derived family of the transfer -function moves out of the emitter. A redundant guard is a guard whose -condition a *dominating* guard already proved. And the loop nest is the third -derivation of a structure the extent scan (`scan_loop_intervals`) and the -token machinery each compute a weaker form of. - -**The extents are not replaced, they are audited.** They key interned token -classes, so renumbering them would renumber every version; `--dump-cfg` -instead reports every extent whose header has no back edge here and every -header with no extent. The two derivations share nothing — a `LoopHead` -marker with a backwards branch to it, against a back edge to a dominating -block — so a disagreement is a bug in one of them, and finding it is the -point. - -Soundness is by over-approximation in **one** direction: more edges means -weaker dominance, which is what every consumer wants. Two places take it. A -spliced call site keeps its generic fall-through edge beside the edge into the -segment, because a walk that declined the splice really does take it. And an -exception landing is made a successor of the **entry** block rather than of -every pc its try range covers, so it is dominated by the entry alone — the -weakest true answer, at a cost of one loop corpus-wide. - ---- - -## 5. Lowerings - -This section is the one a reader most needs in order to predict what code a -given bytecode op produces. Each op is described as a **ladder**: an ordered -list of layers, each with the fact that licenses it, the runtime check that -discharges that fact, the code emitted on the fast path, and where a miss goes. - -Two structural notes first, because they apply to every ladder. - -**Arms end in one of two ways.** - -- *A continuation (side arm).* The arm's block ends in a branch to the version - of the **successor pc** under weaker facts. It never rejoins. The - fall-through keeps the strong fact. `side_arm` (`bbv/arms.rs`) saves and - restores the whole lineage state around the arm (as one `ArmState`, so no - site can save half of it) and steps the track down. - Class-fact arms, typed-load ladders and every arithmetic slow arm work this - way. -- *An in-version merge (diamond).* All arms branch to a merge block that - carries the whole operand stack as block parameters, plus a result and an - `ok` parameter. Emission then continues in the same version at the same pc. - The inline caches and the element diamonds use this internally. - -The choice is not arbitrary. Comparisons use a diamond precisely because every -arm produces the same implication (a boolean), so per-arm continuations would -be merged back together anyway; property reads use continuations because the -arms produce genuinely different facts about the loaded value. - -**Every fact lookup is keyed `(source_id, evid_pc(pc))`.** Inside an inline -splice segment, `evid_pc` subtracts the segment base so analysis facts stay -keyed to the callee's own bytecode offsets. - -### 5.1 Value representation and the cost primitives - -The runtime is `wasm32`, so a boxed `JS::Value` is SpiderMonkey's **NUNBOX32** -layout held in a wasm `i64`: - -``` -value : i64 = (tag << 32) | payload -``` - -Doubles are the raw IEEE-754 bits, all 64 of them, untagged; every non-double -tag lives in the high NaN space so it cannot collide. - -| tag | value | payload (low 32) | -|---|---|---| -| `TAG_CLEAR` | `0xFFFF_FF80` | the double/non-double boundary | -| `TAG_INT32` | `0xFFFF_FF81` | the int32, raw | -| `TAG_BOOLEAN` | `0xFFFF_FF82` | 0 or 1 | -| `TAG_UNDEFINED` | `0xFFFF_FF83` | 0 | -| `TAG_NULL` | `0xFFFF_FF84` | 0 | -| `TAG_MAGIC` | `0xFFFF_FF85` | which magic | -| `TAG_STRING` | `0xFFFF_FF86` | `JSString*` | -| `TAG_BIGINT` | `0xFFFF_FF89` | `JS::BigInt*` | -| `TAG_OBJECT` | `0xFFFF_FF8C` | `JSObject*` directly | - -Because the tags are ordered, three of the four common type tests are -**unsigned range compares, not equalities**: - -``` -tag_eq(v, T) i64.shr_u v,32 ; i32.wrap_i64 ; i32.eq T -- 3 ops -is_number_tag(v) i64.shr_u v,32 ; i32.wrap_i64 ; i32.le_u INT32 - -- true for every double (hi < 0xFFFFFF80) and for int32 -is_double_tag(v) i64.shr_u v,32 ; i32.wrap_i64 ; i32.le_u CLEAR -``` - -The shift/wrap pair GVNs across all tests on the same value, so a three-arm -diamond on one operand pays the shift once. - -Boxing and unboxing costs, which set the price of every representation -decision: - -| operation | emitted | -|---|---| -| box an `I32`/`Bool`/pointer | `i64.extend_i32_u ; i64.or (tag<<32)` — 2 ops | -| box an `F64` known exactly-double | `i64.reinterpret_f64` — **1 op** | -| box an `F64` otherwise (canonicalising) | ~10 ops, branchless: truncate, convert back, compare, detect `-0`, select. It re-tags an integral double as int32 — and explicitly refuses to do so for `-0` | -| unbox a number to `f64` | ~7 ops, branchless `select`, no control flow | -| `ToInt32` of an arbitrary double | 6 ops: `mul 2^-32 ; trunc ; mul 2^32 ; sub ; trunc_sat ; wrap` — NaN and infinities land on 0 correctly | - -The canonicalising box is why an in-int32 *clean* interval additionally proves -an **int32 tag**: canonical boxing re-tags every integral double as int32, so a -value known to be an in-range integer and known not to be `-0` cannot be -sitting in the frame with a double tag. - -There is one deliberate anti-optimisation here worth naming: a boxed-to-f64 -edge conversion takes the *uniform* unbox rather than a fact-directed shortcut, -so identical unboxes at different sites collapse under GVN. The shortcut was -measured and loses. -### 5.2 GetProp - -`emit_get_property` (`bbv/property.rs`). The ladder, in the order the emitter -tries it: - -``` -GetProp x - | - +-- L0 accessor arm -- the name is a known accessor - +-- L1 class-fact arm -- the analysis knows the receiver's layout - | L1a checkless immediate load - | L1b three-bit stamp test, then the load - | L1c SLOTS bit test, then the load - | L1d fused identity + SLOTS test, then the load - +-- L2 `length` arm -- syntactic, string / Array / arguments - +-- L3 charCodeAt/charAt fuse arm - +-- L4 the inline property cache - | W0 monomorphic way, pre-decoded offset - | W1 monomorphic way, general tail (proto holder or dynamic slot) - | W2 the global megamorphic table - | W3 the miss helper - | - +-- (always) the typed-load ladder on the result -``` - -The order is unconditional: L1 pre-empts L2, so an `arr.length` site that also -has a class-fact row takes the class-fact arm. - -#### L0 — accessor arm - -- **Licensed by** `accessor_sites[(sid, pc)]` — a resolved getter script at a - site whose receivers agree on one class — or, failing that, membership of the - name in `accessor_names`, the set of names registered as accessors on *any* - class. The second form arms the fully dynamic arm even where the receiver - never classified. -- **Discharged by** three compares against a 2048-entry accessor cache keyed on - `(receiver shape, atom<<1 | isSet)`: entry match on shape and key, plus a - **holder liveness** check (`holder->shape == entry.holderShape`), because an - accessor redefinition reshapes the holder. -- **Emits** roughly 18 ops and 6 loads, then falls through into an ordinary - call to the cached callee — whose own likely-direct arm re-guards the callee - identity. -- **Miss** side-arms to the successor pc running the ordinary property cache. - -#### L1 — the class-fact arm - -This is the layer that makes the tier fast, and it is the direct consumer of -the stamp (section 2.2). `emit_class_fact_get` (`bbv/property.rs`). - -- **Licensed by** `prop_sites[(sid, pc)]`, which supplies a class-key range - `[lo, hi]`, a **predicted fixed-slot index**, and the field's value mask; or, - where no per-site row exists, by an *exact* class fact already live on the - receiver operand plus the script's own predicted layout. -- Slot indices from the analysis are always **fixed** slots: the emitted - address is the immediate `FIXED_SLOTS_BASE + 8 * slot`. There is no - dynamic-slot form of this arm. That is why the construct site predicts - `nSlots` (section 5.6) — a field that lands in dynamic slots is a field this - arm cannot serve. - -The four sub-arms differ only in how much of the claim is already proven: - -**L1a — checkless.** Licensed by a live `cls_slots` fact on the receiver: some -*earlier guard in this same lineage* tested the SLOTS bit and `refine_src` -wrote the result back. Emits **one `i64.load`**. No word load, no branch, not -even an object tag test — the identity was proven upstream, and a value store -cannot clear SLOTS (the store-side chokes are TYPES-only). There is no miss -arm; re-testing here was measured to fragment a hot loop's code layout (+21% -instruction-cache misses), and block layout is not fixable downstream. - -**L1b — the folded stamp test.** Licensed by a live identity fact where the -TYPES bit is not yet proven. Emits: - -```wasm -i32.load $recv offset=4 ;; the stamp word -i32.const want ;; TYPES|SLOTS, plus RANGES iff the site has a range -i32.and -i32.const want -i32.eq -br_if -i64.load $recv offset=16+8*slot -``` - -Five ops and one load. The RANGES bit joins the mask **only where the site -carries a range claim**, so the fold is the same `and`/compare against a wider -immediate and the range rides in free. This is the one property read that -consumes a RANGES claim: the loaded value is pushed with an interval attached, -which downstream arithmetic then elides overflow checks against. - -`refine_src` then mints durable `cls_shallow` and `cls_slots` facts, so the -*next* read of the same receiver in this lineage is L1a. - -There are deliberately only two arms. A third arm serving SLOTS-only receivers -with a boxed immediate load was measured to bloat hot code units; that -population is served correctly, just more slowly, by the inline cache. - -**L1c — SLOTS bit test.** One load, `and`, `ne`, branch. Used when the site is -untyped or TYPES is already proven. - -**L1d — fused identity plus SLOTS.** Used when a site row exists but the -receiver carries no covering class fact. One class-word load and one fused -compare: - -```wasm -;; exact fact (lo == hi): -i32.load offset=4 ; i32.and 0xFFFF|SLOTS ; i32.eq (k|SLOTS) -- 3 ops -;; range fact (lo < hi): -i32.load offset=4 ; and 0xFFFF ; sub lo ; le_u (hi-lo) - ; (word & SLOTS) != 0 ; i32.and -- 6 ops -``` - -Fusing means a miss says exactly "the immediate arm does not apply" — identity -misses and SLOTS-cleared receivers both belong on the cache route. - -Misses from L1b/L1c/L1d all side-arm to the successor pc running the full -inline cache. - -#### L2 — the `length` arm - -Purely syntactic (the name is `length`). A four-way chain: string -(`i32.load str+4`), Array (walk shape to base shape to clasp, compare against -the Array class, then load `elements_` and the length word, check it fits -int32), mapped or unmapped `arguments` (packed word, overridden bit, argc), and -otherwise the cache. The Array path is 4 loads for the clasp walk plus 2 for -the length, about 12 ops. - -#### L3 — the string char-op fuse arm - -For the names `charCodeAt`/`charAt` on a possibly-string receiver, three -conditions are AND-ed branchlessly: the value is a string, a process-wide fuse -word is clear, and the startup-cached original native's cell is populated. On -success the **cached native's boxed bits are pushed directly** and the property -lookup is elided entirely. - -#### L4 — the inline property cache - -`emit_get_prop_ic_inline` (`bbv/property.rs`). Cache rows live in a reserved -linear-memory region, one row per emitted site, at an address baked as a -placeholder constant and patched once the region base is known. - -The row is deliberately **monomorphic**: polymorphism is served by the -polymorphic sentinel plus the linear-memory mega tables, so a second way would -be reserved-but-never-probed space in a region whose governing constraint is -data-cache locality. - -``` -site row (68 bytes): - +0 recvShape 0 = empty, 1 = polymorphic sentinel - +4 MONO OFFSET pre-decoded own+fixed byte offset; 0 = take the general tail - +8 holderPtr 0 = own property - +12 holderShape - +16 slotEnc bit1 = dynamic, bits[31:2] = index - +20 transition row (48 bytes): oldShape, newShape, slotOff, absSlot, - 4 x (protoPtr, protoShape) -``` - -The probe: - -``` - tag_eq(recv, OBJECT) -- elided if the operand is object-only - shape = i32.load objptr+0 - w0shape = i32.load way+0 - shape == w0shape ? - yes: moff = i32.load way+4 - moff != 0 ? i64.load (objptr + moff) <-- W0: 11 ops, 4 loads - : the general hit tail <-- W1 - no: w0shape == POLY ? probe the megamorphic table <-- W2 - : the miss helper <-- W3 -``` - -The **general hit tail** loads holderPtr / holderShape / slotEnc, selects the -receiver or the holder as the base, re-checks the holder's live shape, decodes -the slot encoding branchlessly (which unconditionally loads `obj->slots_` and -`select`s, so it costs one extra load even for a fixed slot), and loads. That -is about 18 more ops and 6 more loads — which is exactly what field `+4`, the -pre-decoded offset, exists to avoid for own fixed-slot data properties. - -The **megamorphic table** is a direct-mapped 8192-entry side table keyed on -`(shape, atom)`; the atom is a compile-time constant so its half of the hash -folds into an `i32.const`, making the inline hash 6 ALU ops. - -There is **no generation counter on the cache**. Validity comes from the GC -callbacks that zero the whole region on a major GC, plus a targeted zeroing of -transition rows at minor-GC end when any row cached a nursery prototype. - -Monomorphic-to-polymorphic transition is a state machine in the miss helper: -fill way 0 when it is empty or holds the same shape; on a **second distinct -shape** write the polymorphic sentinel and stop paying write-back churn. A site -that settles monomorphic after warmup re-learns after the next major GC zeroes -the region. - -**Miss cost.** The helper call spills every live operand (boxing each and -storing to the frame), calls, and reloads. But the helper returns a **clean -bit**: a miss that was served by a pure slot lookup, a fast `length` path or a -cached add replay restores the full pre-call arm state and takes a *non-dirty* -edge to the successor pc. Only a miss that could have run user code poisons the -lineage. - -#### The result layer: the typed-load ladder - -Every GetProp result goes through one of two forms. - -`push_load_typed` (`bbv/arith.rs`) is the **checked** form: - -| site mask | emitted | -|---|---| -| absent (0) | push boxed, bottom type. **No test at all** — the absence of a claim is itself a claim | -| `0x8000` (object-only) | one `tag_eq(OBJECT)`; fall-through is an object-only lineage; the other arm side-arms to the same pc with a boxed bottom | -| int32-bearing numeric | one `tag_eq(INT32)`; fall-through wraps to an `I32` carrier | -| exactly double | one `is_double_tag`; fall-through is a bare `f64.reinterpret_i64` | - -Two rules are recorded as measured: a mixed `int|double` mask takes the -**int32-first** form unless the analysis explicitly marked the site's double -evidence as fractional-reachable; and the double arm must **not** be widened to -mixed masks (-29% on one benchmark at identical version counts, with a 4.6x -rise in data-cache load misses). - -`push_typed_field` (`bbv/property.rs`) is the **proven-numberness** form, reachable -only behind a passed TYPES guard, so there is no other-type arm at all: an -exactly-int32 mask takes one tag test whose losing side is a bare -`f64.reinterpret_i64`, and an exactly-double mask takes a checkless unbox. - -An int32 default is wrong most of the time on real code, which is why the -ladder does not default to it. - -### 5.3 SetProp - -``` -SetProp x / StrictSetProp x - | - +-- L0 accessor set arm - | - | compute val_is_num -- does this store violate any TYPES claim? - | compute init_mask -- is this a constructor-init store? - | compute range_act -- what does this store owe the RANGES claim? - | - +-- L1 class-fact set arm (checkless / SLOTS test / fused identity+SLOTS) - +-- L2 the inline set cache - W0 monomorphic own-slot store - W1 megamorphic table - W2 add-transition replay - W3 the miss helper -``` - -The set side mirrors the get side, with three additions that carry the whole -maintenance burden of the stamp. - -**`val_is_num` — the choke-elision predicate.** A store must clear the TYPES -bit unless it can be shown not to violate any claim. Three independent licences: - -1. the stored value is statically numeric; -2. the site's own row claims mask 0 for this field and the receiver's class - fact is inside the row's key range — the TYPES claim covers *masked* fields - only; -3. no row is needed: for **every** layout the class fact admits, this name's - mask is absent or 0. An *unknown* layout counts as masked — only a layout - whose masks we hold can prove the field carries no claim. - -The third licence exists because rows are present at only a minority of store -sites in real code. - -**The store choke** (`emit_store_choke`, `bbv/facts.rs`) is emitted after every -inline store arm and is the compiled twin of the engine's own store check: - -``` -1. the RANGE obligation, ALWAYS first: - Nothing -> emit nothing - Clear -> clear the RANGES bit (load16/and/store16) - Check(lo,hi) -> tag_eq(INT32) && lo <= v <= hi ? nothing : clear RANGES -2. if val_is_num -> return. Nothing else is cleared. -3. site mask known and 0 -> return. -4. site mask known and m -> one tag test chosen by m; on failure clear TYPES *and* RANGES. -5. site mask unknown -> is_number_tag(v) ? return - : load the class word; if TYPES set, store it back cleared. -``` - -Step 1 runs *before* the numeric short-circuit deliberately: a statically -numeric value settles TYPES but says nothing about magnitude. - -**The add check.** The transition-replay arm compares the runtime-assigned slot -offset against the site's prediction and clears the SLOTS bit on any deviation. -Three shapes: a known layout with this name at a known position (compare -against that offset); a known layout without this name (harmful only if the -assigned offset falls *inside* the predicted prefix, which would shift it); and -an unknown receiver, where a per-key table of predicted offsets is consulted -and anything unrecognised clears conservatively. - -**The set cache.** Structurally like the get cache, with three differences: the -row's offsets 8 and 12 hold `slotEnc` and `absSlot` rather than holder fields; -there is **no pre-decoded monomorphic offset shortcut**, so every store decodes -the slot encoding and pays the extra `slots_` load; and there is an -**add-transition replay** arm. - -The transition arm validates `oldShape`, a nonzero slot offset, and up to two -prototype `(pointer, shape)` pairs, and **requires the third and fourth -prototype slots to be empty** — deeper rows are punted to the helper, which -replays all four. That restriction was measured: validating four hops inline is -1.2% worse geometric mean, and a depth-split arm is an exact wash. The -conclusion is that the win is not the saved shape loads; it is not replaying -deep adds inline at all. - -One precision cost is worth naming honestly: emitting the set cache -**unconditionally kills every live SLOTS fact in the body**, because the -transition replay's add check may clear the bit on an *aliased* object. It does -this even when the transition arm is not emitted, which is more conservative -than the stated justification requires. - -### 5.4 GetElem and SetElem - -Elements have no name to key a claim on, which changes the economics: a read -site can be *folded* by a claim, but a write site owes the maintenance duty -unconditionally. The analysis gates array claims on exactly that trade -(section 6). - -#### GetElem — `emit_get_element` (`bbv/element.rs`) - -``` -recv_obj = object-only? i32.const 1 : tag_eq(OBJECT) -- each independently elided -key_int = exact int32? i32.const 1 : tag_eq(INT32) -pre = recv_obj & key_int - -pre false -> the string arm, if the receiver may be a string; else the helper -pre true -> - L1 predicted typed-array arm (only if the site has a TA-kind claim) - shape -> baseShape -> clasp, compare against the kind's class - idx continuation at next_pc - L2 dense arm - native-object bit in shape->immutableFlags - elements = obj+12 ; initlen = elements-12 - idx helper - else -> merge - no: L4 polymorphic-TA probe (a pure leaf call, magic = miss) - L5 arguments[i] arm - else the helper -merge: refine the receiver to object-only and the key to int32, - then the typed-load ladder, optionally folded (below) -``` - -Layer by layer: - -| layer | licensed by | discharged by | miss | -|---|---|---|---| -| typed-array | a per-site typed-array kind claim | a **clasp pointer compare** plus an unsigned bounds check against the length slot; detachment is covered because a detached array has length 0 | clasp mismatch falls into the dense arm; out of bounds goes to the helper | -| dense | nothing (syntactic) | the native-object bit, an **unsigned** `idx < initializedLength` (so a negative index fails too), and a magic-tag hole check | hole to the helper; out of bounds to the next arm | -| string `s[i]` | the receiver may be a string and is not proven object | string tag, linear-and-Latin1 flags, `idx store <-- the hot path - else -> frozen ? helper - : load the old element; hole ? the append arm's hole entry - : store - no: the append arm -store: i64.store ; the RANGES store duty ; post-write barrier unless the value - proves it holds no GC pointer -``` - -**The append arm** handles two entries that converge on one probe: a true -append (`idx == initializedLength`, capacity available, element flags clear) -and an **in-bounds store into a hole**, which is add-like in exactly the same -way. The probe is a 512-row cache keyed on the receiver shape alone, holding -two prototype `(pointer, shape)` pairs and an is-array flag; validating those -proves add-safety, since only a prototype indexed accessor or a non-writable -indexed property could block the store, and both force a prototype shape -change. On success it stores, bumps `initializedLength`, and extends `length` -if the receiver is an array. **There is no host call anywhere in this arm** — -the standing observation is that a call in a hot diamond costs several percent -even when it is never executed. - -Sending non-packed stores to the generic helper outright is not acceptable: a -descending fill pattern marks an array non-packed permanently. - -**The element store duty** (`emit_elem_store_duty`, `bbv/facts.rs`) is the -prove-or-clear discipline for RANGES on elements. If the stored value's static -interval already sits inside the claim, **nothing is emitted at all**. Otherwise -the flags half-word is loaded and the RANGES bit cleared only if it was set — -test before write, so the usual case costs one load and never dirties the cache -line holding the object header. When the receiver did not classify, the -obligation is the *intersection* of every array claim in the bundle; a bundle -that claims nothing emits nothing anywhere. - -#### Worked examples - -`this.x` in a method, exact class fact live, `x` at slot 2, mask int32, TYPES -not yet proven, site has a range: - -```wasm -i32.load $recv offset=4 ;; stamp word -i32.const 0x40030000 ; i32.and ;; TYPES|SLOTS|RANGES -i32.const 0x40030000 ; i32.eq ; br_if -i64.load $recv offset=32 ;; 16 + 8*2 -i64.shr_u 32 ; i32.wrap ; i32.eq INT32 ; br_if -i32.wrap_i64 ;; exact int32, interval [lo,hi] -``` - -11 ops, 2 loads, 2 branches. The **next** `this.x` in the same lineage is one -`i64.load`. - -`obj.foo` with no analysis fact, a numeric site claim, monomorphic cache hit: -about 15 ops, 4 loads, 4 branches, plus a merge block carrying the operand -stack. - -`a[i]` in a loop, receiver proven object, key proven int32, dense packed array -with an int32 range claim: about 25 ops, 6 loads — and **no tag tests at all** -on the receiver or the key, because both preconditions constant-folded away. -### 5.5 Call - -#### The calling convention - -Every compiled body has one signature (`translate.rs`, `night_abi_sig2`): - -``` -(i32 cx, i32 sp, i32 argc, i32 retval_out, i32 script, i64 newTarget) - -> (i32 err, i32 eff) -``` - -- `err` — 0 ok, 1 exception pending; the caller routes a nonzero to its handler. -- `eff` — the two-bit **effect-provenance word** (`MUT_THIS` = 1, - `MUT_OTHER` = 2). See "the flag fork" below. -- The **return value is not a wasm result.** The callee stores it through - `retval_out`; the caller reads it back out of the frame. -- `script` is **ignored by every body**, and stays only because the - specialized-call `call_indirect` and the adapter share the signature. A - `JSScript*` held in a wasm local does not survive a compacting GC (`SCRIPT` - is a compacting alloc kind), so `cur_script_value` re-derives it from - `vp[0]` at each use instead — a slot the AOT value stack traces. A function - body reads its callee's script pointer; a global body finds the script - staged in that slot directly, as a private-GC-thing Value. - -The engine-visible funcref table holds a per-script *adapter* with the older -single-result signature, so runtime entries and indirect dispatch never see -multivalue. Compiled bodies precede the contiguous adapter block in the table, -so a compiled-to-compiled indirect call can subtract a patched offset and reach -the body directly, skipping the adapter hop. - -The frame, measured from `vp`: - -``` -vp[0] callee (the JSFunction value; a GLOBAL body has - no callee and carries its JSScript here instead, - as a private-GC-thing Value) -vp[8] this -vp[16 + 8*i] formal i -local_base = 16 + 8*nargs -vp[local_base + 8*j] local j -[env slot] [args-object slot] [new.target slot] [rval slot] -[hoist region] LICM re-derive anchors, rooted -operand_base ... operand-stack spill slots -``` - -The prologue pads missing formals with `undefined` using a branchless `select` -per formal, undef-initialises every local and the rval slot, and — critically — -**lives in a synthetic entry block outside the version table**, so a branch back -to pc 0 cannot re-run it. - -**Argument passing costs nothing.** Before a call the emitter spills every live -operand into the caller's operand region; the top `argc + 2` of those spilled -slots *are* the callee's frame prefix. The callee's `sp` is a pointer into the -caller's own operand area. There is no argument copy. - -Two other pointers matter: `top` is the GC scan limit and doubles as the boxed -out-slot for the return value, and a stack-fits check (`top + frame + 64 KiB -<= limit`) is folded into every arm that enters a compiled body directly. - -**The typed entry.** The top bit of `argc` is a selector. Every body strips it -first thing, so a stale caller-side claim is always safe. If the analysis -claims types for a body's formals, pc 0 has three predecessors: a *proven* edge -(the caller statically established every claim; no tests at all), a -*validation* edge (one tag test per claimed formal, seeding the same facts), and -a *failure* edge that steps to the `Side` track. A validation failure is not a -bailout — the invocation simply rides a weaker lineage through the same body. -On the caller side, a claim counts as proven only if the argument operand's -type implies the claim's *pass-arm fact*. A `Double` claim can never be proven -by a caller, because canonical boxing re-tags integral doubles as int32 in the -frame, so only the callee's own tag test can establish it. - -#### The ladder - -`emit_call_generic` (`bbv/call.rs`). Emitted control flow, in order: - -``` -;; --- pre-dispatch, branch-free --- -recv_bit = classify the receiver for effect accounting -sel_argc = argc | (caller proved the callee's entry claims ? SEL_BIT : 0) -spill_all ; compute frame_base, top ; fits = stack check - -;; --- L0: fuse-guarded static call (only for a global-binding callee) --- -armed = globalVals[bid].fuse == 1 -same = callee_bits == globalVals[bid].value -if (armed & same & fits & ) { - call (cx, frame_base, sel_argc, top, script, undef) -} - -;; --- classify: the per-site call cell, then the chain --- -(funcidx, script, is_native) = classify(callee) - -;; --- L1/L2: direct AOT entry --- -if (funcidx != 0 & fits) { - funcidx == ? call (..., sel_argc) ;; L1 - : call_indirect[funcidx - BODY_OFF](..., argc) ;; L2 -} else { - ;; --- L3: builtin arms, only when no scripted callee was predicted --- - [Array push / pop] [Math unary set] [clz32] [parseInt] [min/max/pow] [imul] - [String charCodeAt / charAt / fromCharCode] - ;; --- L4 --- - if (is_native) native_dispatch(cx, top, frame_base, argc) - ;; --- L5 --- - else night_runtime_call(cx, top, frame_base, argc) -} - -;; --- merge --- -reload ; result = frame[top] ; route err ; pop operands -push the result, through the typed-load ladder if the site has a result claim -``` - -**L0 — the fuse-guarded static call.** Licensed when the callee operand was -read from a global binding that resolved, bundle-wide, to exactly one compiled -callee. Discharged by two loads and two compares: the binding's fuse word is -armed and the live binding value equals this callee. That **replaces the entire -classify** — five validating loads and their branch chain — with four -instructions, and the call is a *static* `call` that wasmtime can inline. -Bindings with conflicting callees across sites, or whose callee did not -compile, never enter the table and their arms are left as dead code. - -**L1 — the per-site call cell plus the likely-callee direct arm.** Two stacked -mechanisms. - -The **call cell** is a three-word row per site: cached callee bits, funcidx, -script pointer. It is probed with three unconditional loads and one `i64` -compare; a hit skips the whole classify chain, which is three dependent loads -(shape, base shape, clasp) plus flag and kind tests. Its soundness rests on -value identity being object identity, plus two rules: only **tenured** callees -are cached (a nursery address can be reused by the next minor GC), and the -region is zeroed by a major-GC callback. A zeroed row cannot false-hit, because -raw bits 0 is the double `+0.0`, whose cached funcidx of 0 routes to the generic -arm anyway. On a *second distinct* callee the row is stamped with a sentinel -value that no boxed object can equal, so a polymorphic site stops paying -write-back churn. - -The **likely-callee arm** then compares the classified funcidx against a -constant patched at link time to the predicted callee's table index, and on -equality makes a static call. If the predicted callee never compiled, the -constant stays at `u32::MAX` and the arm is simply unreachable. - -**L2 — generic AOT entry.** `funcidx != 0` proves a script-backed, -non-class-constructor JSFunction with a compiled body. `call_indirect` to the -body. This arm unconditionally flushes deferred state, records a may-GC effect, -steps the track and kills class facts and carriers. - -**L3 — builtin arms.** Gated on the site having *no* predicted scripted callee, -since a site that predicts a script would only carry dead diamonds. Each arm is -call-free on its hit path and reads arguments straight out of the spilled frame. -Two identity idioms are used: - -- comparing the callee's **`JSNative` pointer** against a pristine slot. This is - necessary because self-hosted code calls intrinsic *clones* of the Math - natives — distinct JSFunction objects wrapping the same native. -- comparing the callee **value** against a cached pristine builtin cell. - -A monkeypatched `Math.sqrt` or `Array.prototype.push` is a different native or -a different value and self-misses. **No fuse hooks are needed in either case.** - -**L4 — the native route.** The classify already established "function class, no -BaseScript". One branch on a value in hand reaches `native_dispatch`, which -calls the `JSNative` with the frame as its `vp`, skipping the generic helper's -arm chain. `Function.prototype.call/apply`, the `defineProperty` intercept and -rope flattening punt back out from *inside* that helper, so correctness never -depends on predicting which native this is. - -**L5 — fully generic.** The complete engine call path: bound functions, -proxies, non-AOT callees, everything. - -`Function.prototype.call/apply` has **no dedicated lowering**; such sites arrive -as ordinary calls with a native callee. The one exception is a recognised -`T.apply(this, arguments)` forward, proved per-script, where the `arguments` -object is elided entirely and a helper forwards the caller's live actuals into -the target. - -Spread calls have no ladder at all: box five operands and call the helper. - -#### The flag fork: keeping facts across a call - -Because a may-GC call kills every class fact in the frame, a hot loop -containing even a trivial call runs permanently on the `Dirty` track. The -answer is not a weaker dirt rule (section 4.11 explains why a weaker dirt -rule costs more than it saves) but a **proof carried in the callee's -return**. - -The effect word is an SSA value threaded through the version graph as a -trailing `i32` block parameter — not a frame slot, so the frame layout is -untouched, the GC never sees it, and there is no per-invocation initialisation. -Only bodies that some caller *demands* thread it; demand is computed by walking -the call closure from read-only-scanned monomorphic callees. - -A body's static scan classifies it as `ReadOnly` (heap-read-only on its inline -paths; calls and constructs are allowed because their effects arrive -dynamically), `StoreOnly` (also property and element stores, plus literal -construction, whose receiver classification is emission's job), or `Fail`. -Return shapes follow: a threaded body returns the live accumulator on every -lineage — because the bytecode scan cannot see a *spliced* callee's inline -stores, so emission is the accounting of record — and a non-threaded body -returns a provisional zero on a clean lineage that the pass end **revokes** to -the body's classified write word if any version turned out to write. - -That revocation carries more than it looks like. A non-threaded body's word is -a compile-time constant, so everything it claims must be provable statically or -revoked, and "no callee of mine wrote anything" is not statically provable — -`ReadOnly` permits calls precisely because their effects were expected to -arrive dynamically. A call does not step the track, so the revocation carries -this instead: every `or_flags_*` that finds no accumulator records its bits -body-globally (`untracked_flags`) and the pass end ORs them in, which revokes -the constant in any body that calls anything at all. Without that, a -scan-passing body nobody's demand closure reached could return a zero word -after calling something that wrote heap, and its caller's fork would believe -it — a miscompile this mechanism prevents. - -At a static-target call arm the emitter then forks: - -``` -clean = ok & (flags == 0) -clean -> restore the pre-call arm state wholesale, re-derive the addresses of - possibly-moved operands from their frame slots (a clean word no longer - implies no-GC, since quiet allocs do not saturate it), and continue at - next_pc as its own lineage -- facts, track and carriers intact -dirty -> OR the folded word into the accumulator, take the ordinary merge, - and LEAVE THE OPT TRACK there -``` - -The track step on the merge is what makes the fork mean anything. Both sides -continue at the same program point, and a program point has exactly one -prediction; if the merge stayed on `Opt` the join would erase precisely the -facts the clean arm restored, and the mechanism would be dead. It is stepped -only where a keep continuation actually exists — a site with no fork has no -`Opt` lineage to protect, and stepping there is the old unconditional -`call_stepped_track`, which costs a weakly predicted tail its `Opt` code for -nothing. The merge, having failed the callee's runtime proof, may still -re-prove the successor's prediction fact by fact: the just-in-time on-ramp of -section 4.8. - -Folding is a perspective translation and is subtle: the callee's `MUT_THIS` -names *the callee's* `this`, which is this frame's `this` only when the call -receiver provably is it. For a **fresh** receiver, the callee's own-this writes -hit an object no caller fact can reference, so `MUT_THIS` is dropped entirely. -Otherwise it widens to `MUT_OTHER`. Crucially the fold maps zero to zero, so -clean tests are unaffected. - -The population gates are measured, not guessed: read-only-callee forks land -clean essentially always (one benchmark: 30,544,418 clean against 10 dirty), -store-only-callee forks took the dirty arm **100% of the time** corpus-wide, and -a site on an already-dirty lineage has nothing to save. - -There is a second, cheaper form: a call site whose call-free numeric builtin arm -hits executes no helper and touches no heap, so **arm selection is the proof** -and its exit is a clean continuation with no flag test at all. Without it, a -`parseInt` coercion at a hot function entry left every downstream loop header -dirty. - -### 5.6 Construct - -`emit_construct_classify` (`bbv/call.rs`). The operand frame is -`[callee, this-placeholder, args..., newTarget]`. - -``` -funcidx = classify(callee) (or reuse one a splice already ran) - -funcidx != 0 -> - is_ctor & is_ordinary_kind & (newTarget == callee) & fits ? - allocate `this` (below) - splice it into frame[1] - RE-DERIVE the callee, its script and the real newTarget from the frame - -- create_this may have moved them - static call to the predicted ctor, or call_indirect - : the generic helper -funcidx == 0 -> - [new Array() arm] or the generic helper - -merge: result = is_object(ret) ? ret : the reloaded `this` - mark the result FRESH iff the ctor provably returns `this` -then: emit_construct_class_guard -- one word compare on the result's stamp -``` - -**Allocating `this`** goes through a per-site cell holding a shape, slot and -element words, a nursery header, the constructor's cached shape, an IC -generation, and the cached prototype pointer and slot encoding. The fast arm -validates the cell, the constructor's live shape, the generation, and then -**re-reads `C.prototype` live** and compares it to the cached pointer — which is -what catches `C.prototype = ...` reassignment. Then it bumps the nursery cursor -and writes the header, shape, stamp word, slots and elements. That path -**cannot GC**, so it contributes zero effect; the reactor fallback saturates. -The per-path effect delta rides a block parameter precisely so the bump path -does not inherit the fallback's saturation — which is what lets a construct -site take the clean fork. - -**How much is known statically.** Three tiers: - -| tier | nSlots | stamp word written at allocation | -|---|---|---| -| the site resolves monomorphically to a constructor with a predicted layout | that layout's full row length | the early-key form of its layout key | -| the site has a shared-constructor record (the `Class.create()` idiom, where many classes share one constructor script) | the resolved class's row length | that class's key | -| neither | read from a per-funcidx region indexed by the *classified* funcidx at runtime | a keyless seed with all three validity bits set | - -Predicting `nSlots` matters because the engine's own constructor-body property -count estimate can land predicted fields in *dynamic* slots, which the -fixed-slot arms cannot serve. - -The keyless seed still sets SLOTS, because the delegate flows' *static* add -checks maintain it: positions are absolute, so an inconsistent flow self-detects -by position mismatch, and every unchecked add path clears conservatively. - -**The `new Array()` arm** is licensed by an unresolved callee, zero arguments, -and the script naming `Array`; discharged by an identity compare against the -pristine Array constructor cell; and emits exactly what `[]` builds — a quiet -allocation, so the arm keeps its facts and carriers and marks the result fresh. - -**Constructor-init stores.** Two mechanisms keep a freshly built object's -stamp claims alive through its own constructor: - -- the *init mask* discipline: `this.f = v` inside a stamping constructor or an - init delegate keeps the constructing sentinel and does a conform-check-or-clear - on the masked field rather than a blanket TYPES clear. The receiver scope is - own-`this` in the root body (with a prologue guard covering the foreign-`this` - corner reached through `.call`), or own-`this` inside a **construct splice**, - where the receiver is the freshly created object by construction. -- the *add check* described in section 5.3, which maintains SLOTS across adds. - -**Exit stamping.** At every return of a registered stamping constructor the -body writes the class word, carrying forward whichever of the three validity -bits survived construction. A two-phase constructor's init delegate *restamps* -at its own returns, advancing a prefix key to the full key so full-only field -guards start hitting. A first stamp is classified `MUT_THIS` — nothing a caller -holds can be falsified, since the constructing sentinel fails every identity -guard on a not-yet-stamped object — while a restamp advances a *guardable* -prefix key, so pre-existing alias facts are real and it must widen to -`MUT_OTHER`. - -Finally, `emit_construct_class_guard` runs immediately after every non-spliced -construct site: one word compare on the result's stamp against -`key | TYPES | SLOTS`, turning "the constructor's exit stamp usually set this" -into a fact that rides the value into its local. Every later field store on it -then elides the choke and every later load is checkless. This is the -one-guard-buys-the-lineage discipline applied where the lineage is *born*. - -### 5.7 The inline splice - -This is the most intricate machinery in the compiler, and it is worth being -precise about what it is *not*. There is no separate inliner IR, no recursive -translator descent, and no callee CFG import. A splice **maps the callee's -bytecode into a synthetic pc space above the root script's** and lets the -ordinary BBV workqueue emit it, with a *frame view* swapped in whenever the -emitter's current pc lands in that space. - -#### Admission - -`inline_candidates` (`bbv/inline.rs`) returns the predicted callees that pass, in -evidence order: - -``` -no analysis evidence at this site -> decline (never speculate blind) -depth >= 8 at a loop-interior site, 4 else -> decline -caller needs an arguments object -> decline -8 splice sites already in this body -> decline -body already over 100k SSA values -> decline unless every target - is <= 160 bytecode bytes, and - then only to 250k -site inside a non-Loop try-note range -> decline -more than 4 targets -> decline -per-target size cap: 200 bytes in a loop, 150 outside (monomorphic); - 500 bytes (polymorphic) -constructs: monomorphic only, plus a transitive closure budget of 300 -``` - -Per target: non-empty bytecode within the cap, not generator or async, not -mapped-arguments, no non-`Loop` try notes, no loops if the *site* is inside a -loop, no environment ops, no `arguments`, no actual-args access, no -`new.target`, and **every op on an explicit allowlist**. The allowlist is much -narrower than the compiler's overall op coverage, because an unsupported op -inside a splice would sink the entire caller. - -Note what is **not** required: no purity, no no-throw, and **no arity match**. -Over-application is sound precisely because the surplus actuals land on the -callee's locals region and the prologue's undef-init clobbers them — which is -observable only through `arguments`, a rest parameter, or actual-args access, -every one of which admission already rejects. - -`splice_closure_cost` prices what a construct splice transitively pulls in, -because every other rule prices it in isolation. Bytecode bytes are the wrong -currency: a nested call lowers to a whole classify diamond (once per *version* -of a segment) while an add lowers to a handful of values, and a loop inside a -spliced callee is emitted once per version *and* duplicated per skipped-header -context by the backend on top of that. - -The budget is scoped to constructs, and the reason is sharper than "size": the -*winning* call splices in one benchmark are strictly larger than the *losing* -construct splice in another on every currency. Size is not what separates them, -**benefit** is. A constructor splice earns exactly one thing — the field-init -stores running in the caller against a provably fresh `this` — so its closure -has a small fixed payoff and deserves a small fixed budget. A call splice's -payoff scales with what it removes, so it keeps the generous per-target caps. - -#### The frame layout of a spliced body - -At the call site the caller has `len` operands, the top `need` of which are the -call operands. The hit arm: - -``` -popped = the top `need` operands -n_parent = the remaining caller operands -for i in 0..n_parent: store box(stack[i]) at caller.operand_base + 8*i ;; ROOT them -child_base = caller.operand_base + 8*n_parent -for (i, o) in popped: store box(o) at child_base + 8*i ;; the frame PREFIX -;; then, per target, inside its own hit arm: -pad missing formals with undefined (compile-time-known argc, so no select loop) -undef-init every local and the rval slot -``` - -So the child frame is laid down **in place, at the top of the caller's operand -region** — exactly the trick a non-spliced direct call uses, minus the call -instruction. Entering the frame view then computes the callee's `local_base`, -rval slot and a fresh operand base above them, so a nested splice recurses the -same way. That block of stores *is* the callee's frame prologue, hand-inlined -and stripped of everything admission made impossible. - -Inside a segment, `GetArg`, `FunctionThis` and the constructor exit stamp all -read from the child frame's offsets; locals are addressed off the view's -`local_base`, so no code needed changing. - -Locals and arguments are real frame slots in the child region with the same -carrier layer on top, but **cross-frame seams carry nothing**: the callee's -locals were just undef-stored, and after a return the caller's carriers are -stale because the callee may have triggered a GC. The frame is the truth on -both sides of the seam. - -#### The guard chain - -``` -funcidx = classify(callee) ;; the SAME cell and chain a real call uses -generic_blk: ;; the miss arm, emitted first - emit_call_generic(...) ;; the classify repeats -- one dead compare, - br dirty_edge_to(next_pc) ;; because the cell hits -chain_blk: - - for each target: - funcidx == ? hit : next target (or generic) - hit: - swap in the callee's entry facts: - args_ctx[0] = the call site's `this` operand fact - args_ctx[1+i] = argument i's fact - caller_locals / caller_args = the caller's own vectors - br cont(segment base) ;; an ordinary BBV edge -``` - -The entry-fact inheritance is the point: the callee's entry version inherits -the *call site's* proven types directly, which is what makes a splice more than -a copy of the callee body. - -Polymorphic splices are load-bearing — dropping them measured -19% on one -benchmark. The per-caller cap of 8 sites is an instruction-cache tuning with -counters to prove it: at cap 8 one benchmark executes 5% *more* instructions -than at cap 64 and runs 5.5% *faster* (IPC 4.04 to 4.49, i-cache misses per 1k -instructions 7.81 to 5.60). Below 8 it inverts. Cap 4 puts the hot 90% -footprint under L1i and still scores worse than cap 8's larger footprint, so -**footprint is not the binding cost** — call overhead is, until code layout -takes over. - -#### Construct splices - -Monomorphic only, and three deltas: the miss arm hands the already-computed -funcidx down to the generic construct so its ladder is one compare rather than -a repeat; after the funcidx hit there is still a constructor-ness check, because -a funcidx hit proves a *script-backed JSFunction* and constructor-ness is a -JSFunction flag; and `create_this`'s out-slot sits past the child frame, in -dead space the locals init subsequently owns. - -Two subtleties: the `newTarget` operand slot is dead once `this` exists (new -target users are rejected by admission) and is deliberately clobbered by the -argument padding; and the entry argument facts have **class facts stripped**, -because `create_this` may GC, which kills every durable class fact, and the -operand snapshot predates that call. - -#### Returning - -There is **no return instruction**. A return inside a segment is an *edge*: - -``` -if the segment is a construct: emit the ctor-exit stamp -emit any delegate restamp -rb = box(retval); constructs substitute is_object(rb) ? rb : frame[this] -restore the caller's fact vectors from caller_locals / caller_args -stack = [] ;; rebuild the CALLER's operand stack -for i in 0..caller_depth: - stack.push(load(caller.operand_base + 8*i)) ;; RELOAD -- the GC updated in place -stack.push(retval, carrying the callee's PROVEN type and range) -br edge_to(ret_pc) -``` - -Two things make this work. The caller's operands were rooted at the call site -by the shared frame build, and the GC tracer updates those slots in place, so -reloading them is exact. And the caller's *frame facts* rode the segment -context and are restored here — sound because caller frame slots are private: -no environments in inline callees, no debugger, no generator callees (a -splice cannot cross a suspend), so no callee can reassign them. Class facts among them were already killed by any -may-GC call inside the segment. - -The retval carries the callee's proven type and range across the seam. That is -a genuine cross-frame fact transfer, and it is what a splice buys beyond -removing the call. - -#### Exceptions inside a spliced body - -There is no deoptimisation and no bailout anywhere in this tier, so the only -question is a genuine JS exception. A helper's error return routes to the -enclosing handler, which is found by walking the *current script's* try notes — -under a frame view, the **callee's**. Admission guarantees the callee has no -non-`Loop` try notes, so there is never a handler, and the error block returns -from the whole compiled body with the exception pending. - -That is only correct because the caller cannot have a handler over the call -site either — which is exactly why admission rejects a site inside any -non-`Loop` try-note range of the *caller*. An exception at a synthetic segment -pc could not match the caller's note ranges, so the site must have nothing to -match. Together the two rules make "throw out of a splice" equal to "throw out -of the whole compiled body", a state the engine already handles. - -#### Splices and the version graph - -Segment pcs are just pcs, so the version machinery works unchanged, with one -mapping: a synthetic pc's loop membership is tested against **every enclosing -call site, innermost first, each in its own pc space**. Splice code is -loop-*interior* code of the caller; without that mapping the whole splice tail -reads as a side entry and its back edges erode to the generic class. Collapsing -to the root-space site alone made every nested splice inside a callee loop read -as a side entry, whose return edge then gave that loop's cycle a second entry — -producing the only irreducible edges observed in one benchmark. - -Recursion is not specially detected. It is bounded structurally: segments are -memoised on `(call pc, callee)`, so a self-recursive callee spliced into itself -needs a fresh call pc at each level and the depth cap terminates it. - -A loop-bearing callee spliced at a loop-interior site is rejected outright: the -two-level version-loop nest is what the backend's duplication amplifies, and one -such pair lowered to a 44 MB function from a 13k-block body, past the engine's -function size limit. -### 5.8 Binary arithmetic - -`emit_addsub_op` (`bbv/arith.rs`) is the template; multiply, divide, modulo, -the bit operations, increment/decrement and negate follow the same shape. Every -emitter first computes the **result interval** from the operands' intervals -using the algebra of section 7, and that interval is what selects the rung. - -#### Add and Sub - -**Rung A — both statically exact int32.** - -- *Fact:* both operand masks are exactly `PRIM_INT32`, not object, no - abstractions. -- *Check:* none for the type. Overflow is delegated, and is itself elidable. -- *Emits:* two sign-extensions (free if the operands are already on the - exact-integer track) and one `i64.add`, then the overflow split. - -**Rung B — the exact-integer track.** - -- *Fact:* the *result* interval is **clean** (provably not `-0`) and inside - `+/-2^53`, and both operands materialise as exact `i64` without an unbox - diamond. -- *Check:* none. The interval algebra is the proof. -- *Emits:* **one `i64.add`.** No overflow test, no conversion, no branch. The - result is carried as `I64` and its low 32 bits *are* its `ToInt32`, so - consumers can wrap for free. -- *Miss:* there is no failure mode. This rung has none. - -**Rung C — both proven numeric.** One `f64.add` after conversions that are free -for operands already carried as `F64`. The result mask follows one rule: only -when **both** operands are proven exactly-double does the result claim exactly -double (which makes its later boxing a single reinterpret); anything with an -int32-bearing operand keeps the wider numeric mask so the chain stays -canonicalised and visible to the int32 arms downstream. - -**Rung D — nothing proven: the three-arm tag diamond.** - -``` -ab = box(a) ; bb = box(b) -br_if both_int32(ab,bb) -> int_blk ;; 7 ops -br_if both_number_tags(ab,bb) -> f64_blk ;; 7 ops, shift/wrap GVN'd with above - -> slow_blk - -int_blk (FALL-THROUGH, stays on the current track): - wrap, extend, i64.add, then the overflow split -f64_blk (side arm -> Side track, continues at succ_pc): - two branchless unboxes, f64.add, canonicalising box ~35 ops -slow_blk (side arm -> Side track, continues at succ_pc): - spill, call the helper, reload, route errors -> Dirty track -``` - -Every arm carries the result interval forward. That is not optional: the -successor join takes the meet across arms, so an arm that drops the interval -kills the slot's fact for *every* lineage arriving at that pc. - -`+`'s slow arm pushes a **bottom** type, because `+` may concatenate; `-`'s -pushes numeric-or-bigint. Even so, the presence of a `Some` interval proves both -operands were numbers, so where the interval exists the concatenation case is -statically unreachable and the fact survives the join. - -**The BigInt bit and the dynamic-code fuse.** Dropping the BigInt bit from a -helper result matters far past the one op: `is_numeric` is what admits the -*next* op's unboxed f64 path, and the BigInt bit alone disqualifies it. Whether -the bit is needed is only partly a static question — the module's own text is -scanned (`module_is_bigint_free`), but source compiled at runtime is not -scannable in principle. So when the text is clean the slow arm **splits on the -dynamic-code fuse** (`Helpers::dyncode_fuse_word`, one load and one `i32.eqz` -of a night-owned word the C++ runtime blows from `ScriptSource::assignSource`): -the intact edge claims `Int32|Double` and the blown edge continues at the same -successor pc, in its own version, carrying the BigInt bit — which is simply the -lowering a module with static BigInt evidence gets everywhere. - -A static type claim has no branchless runtime guard. This tier has no deopt -landings, so the *only* sound recovery from a claim that may be false is a -second typed continuation, which is exactly what versioning is for. Measured -across the benchmark corpus, the second lineage costs well under 2% of module -bytes even on the heaviest case. - -**The overflow split** (`int_result_or_ovf`, `bbv/arith.rs`): - -``` -w = i32.wrap_i64 sum -if the result interval is CLEAN and inside int32: - push w as I32 with the clamped interval. ZERO CHECK CODE. -else: - fits = (i64.extend_i32_s w) == sum - fits ? push w as I32 (clamped interval) - : side arm -> f64.convert_i64_s, reinterpret; pushed as a boxed - integral double, PRIM_DOUBLE, continuing at succ_pc -``` - -The overflow exit is an **integral double boxed as raw f64 bits**. The -cleanliness requirement is real: int32-tagged operands are never `-0`, so a -flagged interval can never soundly elide the check. - -#### Mul - -The governing rule is **disregard overflow**: an int32 product is *typed* exact -int32, with the rare overflow and the rarer `-0` taken by side arms, rather than -typing every downstream use "might be a double" and poisoning the whole -consuming chain off the exact-int32 track. - -The int32 rung has a **three-way interval split**: - -``` -prod = i64.mul (exact, at most 62 bits) -result interval inside int32 and CLEAN -> checkless: wrap and push. - Both the overflow half AND the -0 - test are dead. -result interval inside int32 but flagged -> only the slim -0 test survives -otherwise -> the full overflow + -0 ladder -``` - -The `-0` test is `product == 0 && (a ^ b) < 0`. The exit arm redoes the multiply -in `f64`, the only form that yields both the correctly-rounded wide product and -`-0` itself. Without the interval elision, this ladder can cost a quarter of a -multiply-heavy kernel's cycles. - -The exact-integer rung applies to multiply as well, and there the clean-interval -test does double duty: it proves the product is an in-domain integer **and** -that it is never `-0`, which is the one case the f64 form would have had to -reproduce. - -#### Div, Mod, Pow - -**Div has no integer rung at all** — division is never exact-int in any -rendition — so it is either `f64.div` under a proven-numeric fact or a two-arm -diamond. NaN, infinities and `-0` all come out of `f64.div` correctly, and the -canonicalising box explicitly refuses to re-tag `-0` as int32. - -**Mod does** have an exact `i64.rem_s` rung, and its admission *is* the interval -rule: the dividend must be provably non-negative and unflagged (otherwise the -result could be `-0`) and the divisor range must exclude zero (otherwise NaN). -Under those two conditions `i64.rem_s` neither traps nor disagrees with JS's -dividend-signed rule. Otherwise a leaf helper computes the correct `fmod`. - -**Pow** has no ladder: box both operands and call the helper. - -#### Bitwise operations - -JS semantics are `ToInt32` on both operands, and wasm shifts already mask the -shift count mod 32 exactly as JS does. Three rungs: both operands wrappable to -int32 (one wasm instruction, no check); both numeric (six-op `ToInt32` each, -then one instruction); or a three-arm diamond. `>>>` produces a `u32`, which -does not fit int32, so it rides the exact-integer track with an interval of -`[0, 2^32)`. - -Bit operations **cleanse the `-0` flag** — `ToInt32(-0)` is `0` — which is how a -masked or shifted chain recovers a clean interval after an operation that -flagged one. - -#### The four epistemic states, side by side - -For `a + b` at one pc: - -| what is known | emitted | -|---|---| -| int32 with intervals proving no overflow | **1 instruction** (`i64.add`), or 0 extra if both are already carried as `I64`. No tag test, no overflow test, no branch, no block | -| int32, overflow unproven | ~5 extra instructions and one never-taken branch to a cold block | -| numeric but not int32 | no test at all, but the result is an `F64` carrier that costs a ~10-op canonicalising box at every boxed boundary unless the exactly-double fact holds | -| unknown | ~11 test instructions and 4 blocks on the fast path, plus two cold arms (~35 ops for the f64 arm; spill/call/reload for the helper arm) | - -#### String concatenation - -There is **no inline concatenation arm**. `+`'s non-numeric arm is a full helper -call whose result is bottom-typed on a `Side` lineage. The specialisation lives -in the runtime helper, which checks for string-string first and calls the -engine's string concatenation directly, skipping the generic `+`'s -`ToPrimitive`/`ToNumeric` dispatch on both operands. - -### 5.9 Comparisons - -`emit_compare_op` (`bbv/compare.rs`). Unlike arithmetic, comparisons use an -in-version **diamond** rather than per-arm continuations, because every arm -produces the same implication — a boolean — so continuations would be merged -back immediately anyway. The diamond is deferred-spill: the live operand stack -is snapshotted at entry and re-bound at the merge, so only the arm that actually -calls a helper pays a spill. - -The ladder: - -1. **Both exact int32** — one `i32` compare. Under proven int32 tags, loose and - strict equality coincide, so both map to the same instruction. -2. **The exact-integer track** — one `i64` compare, admitted when at least one - operand is already carried as `I64` (two `I32` operands take the cheaper - int32 form instead). -3. **Both numeric** — one `f64` compare. **This is where NaN is handled, and it - is free**: the wasm float comparisons are all false for NaN and `f64.ne` is - true, which is exactly JS. IEEE serves both loose and strict number compares. -4. **Strict equality by raw boxed bits** — licensed when neither side may be a - double, not both may be strings, and not both may be BigInts. One `i64.eq`. - Object identity, nullish comparisons and int/bool all reduce to this. -5. **`== null` / `== undefined`** — an OR of two tag compares. The - `document.all` "emulates undefined" check is elided entirely when the other - side is a proven non-GC primitive, and is otherwise behind a runtime fuse - word. -6. **The tag-guarded diamond** — int32 arm, f64 arm, then an equality ladder - (for equality kinds) or the helper. - -The **equality ladder** is worth spelling out, because it is where the -representation pays off. Both operands have already been proven non-number by -the diamond's tests, so for `===`: - -``` -both strings -> the string equality arm -both BigInts -> the helper -otherwise -> i64.eq on raw bits. Done. -``` - -No type peel at all: raw bits are exact for *every* remaining pair — object and -symbol identity, canonical booleans, null, undefined, and every mixed-tag pair -— except the two content-equality types. If the module's own text is provably -BigInt-free the both-BigInt tag test is dropped for one load and one test of -the dynamic-code fuse instead (section 5.8): while it is intact the bits are -exact unconditionally, and once it is blown every pair that reaches here takes -the helper, which decides content equality for real. Loose `==` keeps a peel -(both-boolean, both-object, both-string) because `1 == true` is true with -different bits. - -Without this ladder every object `===` calls the compare helper, which on -object-heavy code is among the hottest generic-helper sites. - -The **string equality arm** is a chain of cheap disproofs: pointer identity -(equal), length inequality (unequal), both-atom (unequal, since atoms are -deduplicated), and only then the helper. A linear character comparison leaf is -deliberately *not* emitted, because it is only reachable for a *proven* string -operand, which a bottom-typed operand never is. - -The helper arm leaves the version entirely and continues at the successor pc -rather than joining the diamond — keeping the call out of the loop body is what -admits LICM on compare-conditioned loops. - -### 5.10 ToBoolean - -`to_bool_i32` (`bbv/frame.rs`), used by `Not`, `JumpIfFalse`, `JumpIfTrue`, -`Case`, `And` and `Or`. (`Coalesce` does not use it; it tests null and undefined -tags directly.) - -``` -Repr::Bool | Repr::I32 -> the value itself, ZERO INSTRUCTIONS - (wasm br_if truthiness IS ToBoolean here) -Repr::I64 -> i64.eqz ; i32.eqz -- 2 ops -Repr::Boxed, exact int32 - or exact boolean -> i32.wrap_i64 -- 1 op -everything else -> the full five-way tag ladder -``` - -The full ladder is a real diamond with a merge parameter: - -``` -hi = high word of the boxed value -hi double: (d != 0) && (d == d) ;; nonzero AND ordered -hi == TAG_STRING -> length != 0 -hi == TAG_BIGINT -> a helper call -otherwise -> low32 != 0, AND NOT emulates-undefined -``` - -The last arm covers undefined, null, boolean and object in one shot: undefined -and null have payload 0, a boolean's payload is 0 or 1, and an object pointer is -never null. The `document.all` check is **triple-gated**: only for object tags, -then behind a process-wide fuse word (if the fuse is clear the whole check -yields 0 with no memory touched beyond the fuse), and only then a walk of -object to shape to base shape to class to flags. - -One gap worth recording: there is **no `Repr::F64` fast case**, so an f64 -carrier at a branch pays a box plus the entire tag ladder including a -statically-true branch. And the routine never consults the operand's interval, -so a value with a proven non-zero interval still emits the full test. ---- - -## 6. The analysis - -`compiler/src/likelier/`. A whole-bundle, context-sensitive, inclusion-style -type analysis producing *likely facts*. It is optimistic by design, and that is -only sound because nothing it says is trusted: an unresolved flow contributes -*nothing* to a fact join rather than poisoning it to "any", and where evidence -runs out the analysis emits no claim rather than a weak one. - -`likelier::analyze` runs five phases: - -1. **scan** — one bytecode pass per script, producing a context-free constraint - graph plus side tables; -2. **seed** — the snapshot image is loaded as the *initial state* of the shared - cells; -3. **resolve shared constructor sites** — a syntactic and snapshot-concrete - pre-pass for the "one constructor script, many classes" idiom; -4. **solve** — instantiate every script at the generic context, then drain one - worklist to a fixpoint; -5. **emit** — project the fixpoint into `facts::LikelyFacts`. - -### 6.1 The cell value: `TypeSet` - -```rust -// likelier/types.rs -struct TypeSet { - prims: u16, // primitive tag bits + the unresolved-evidence bit - fns: FnSet, // a bounded set of callable identities - obj: ObjPart, // object / class identity - range: Range, // numeric magnitude class (opsem::Range) - hiv: HeapIv, // numeric value interval -} -``` - -Bits 0-5 of `prims` alias the codegen's primitive encoding exactly — one -alphabet across the whole compiler. Bit 14 is `P_UNKNOWN`, the distinguished -**evidence** marker: "an executed-but-unresolved flow produced this value" (a -megamorphic call result, an unmodeled builtin, a read off a lost receiver). It -is emphatically *not* the same as "nothing ever flowed": it propagates like a -primitive bit, poisons value claims, and is invisible to callee and object -resolution. It must never appear in an emitted mask. - -Nullability *is* representable — null and undefined are ordinary bits — but is -deliberately **not exportable as a fact**. A `null|object` read stays bottom, -because a type in the codegen is a proof and the analysis only has a -prediction. - -`FnSet` is a sorted vector with a saturation flag, capped at 8 identities; past -the cap the identities are dropped and the set becomes "megamorphic", which no -longer drives call resolution. Three identity spaces share the integer: -ordinary script ids, named natives, and the **allocating builtins** (`Array`, -the typed-array constructors) — so `var Vector = Array` flows array-ness through -cells like any other callable value. - -`ObjPart` is the class-identity lattice: - -``` - AnyObject top - | - AnyOf(region root) bounded polymorphism, flow-scoped - | - ClassAny(class) some instance of one class - | - One(abstraction) exactly one heap abstraction - | - Empty bottom -``` - -An `AnyOf` names a **region** — a union-find class over classes that actually -met in a join — and is consulted *only* by call-target resolution. Field masks -never consult it: a read off a precise `One` receiver must not see sibling -instances' values, and a read off a region must not see the merged set's cells. -That rule has a name in the source ("the mega-cell lesson") and it is the single -most important structural constraint in `heap.rs`. - -### 6.2 The lattices - -| lattice | elements | order | join | where | -|---|---|---|---|---| -| primitive mask | subsets of 7 tag bits plus the unknown bit | subset | union | `types.rs` | -| callable set | up to 8 ids, or saturated | subset, saturated is top | union with saturation | `types.rs` | -| object identity | the five points above | as drawn | table below | `types.rs` | -| magnitude | `I32 < I53 < Top` | chain | max | `opsem.rs` | -| heap interval | `Empty < In(lo,hi) < Num < Any` | as drawn | hull, quantized | `types.rs` | - -The object join table: - -| a \\ b | Empty | One(y) | ClassAny(d) | AnyOf(s) | AnyObject | -|---|---|---|---|---|---| -| **Empty** | Empty | One(y) | ClassAny(d) | AnyOf(s) | AnyObject | -| **One(x)** | One(x) | equal? One(x); snapshot-preferred; same class? ClassAny; else AnyObject | class(x)==d? ClassAny(d) : AnyObject | AnyObject | AnyObject | -| **ClassAny(c)** | ClassAny(c) | (symmetric) | c==d? ClassAny(c) : AnyObject | AnyObject | AnyObject | -| **AnyOf(r)** | AnyOf(r) | AnyObject | AnyObject | r==s? AnyOf(r) : AnyObject | AnyObject | -| **AnyObject** | AnyObject | AnyObject | AnyObject | AnyObject | AnyObject | - -The engine then applies two **rescues** on top of this table: two array-classed -parts meet as one class (unioning their site classes), and two otherwise-classed -parts whose pure join would be `AnyObject` meet as an `AnyOf` region — again -unioning. - -`TypeSet::join_from` (`types.rs`) is the raise operator, and it is -**directional by construction**: it mutates the receiver in place and returns -*whether the receiver grew*. That boolean is the fixpoint's only change signal. - -Two honest departures from a textbook framework, both deliberate: - -- **The object join is non-associative in one corner.** When two distinct `One` - abstractions meet, a snapshot abstraction is preferred over an allocation-site - one. In a three-way snapshot-plus-two-allocations meet the answer depends on - worklist order; the code notes it is deterministic under the FIFO worklist. -- **The join has a global side effect.** The union-find rescues permanently - merge classes. Cells already holding a region label are not re-raised; their - label's meaning changes under them, and emission resolves labels to current - roots at emission time so later unions never poison earlier evidence. It is - monotone in region size, but it is not a pure lattice operation. - -**Finite height** is argued explicitly: 7 primitive bits, 9 callable-set -growths, 3 object raises plus one snapshot absorption, array relabels, and the -interval ladder's roughly 36 rungs per bound. The product bounds a cell at 768 -changes, which is asserted in debug builds. - -### 6.3 The heap - -There is **no oracle and no consultation point.** The snapshot is loaded as the -initial state of the same cells the program's own writes join into. - -**Cell identity.** Per-context rows for `Var`, `Arg`, `This` and `Ret`; -context-free cells for everything shared: - -| cell | keyed on | role | -|---|---|---| -| `GName` | a name | a global binding | -| `Aliased` | (scope, slot) | a closure environment slot | -| `Field` | (abstraction, name) | the per-abstraction field cell | -| `ClassField` | (class, name) | writes through a class-typed receiver | -| `ClassView` | (class, name) | **the read view**, fed by standing links from every `Field` of that class and from `ClassField` | -| `ThisField` | (script, name) | a script's accumulated `this.name = v` evidence | -| `ProtoSentinel` | abstraction | raised when a prototype link is installed, so chain reads that dead-ended re-fire | -| `ArrayElemsUnion` | — | the bundle-wide union of every array's element cell | -| `TableArgJoin` | (abstraction, index) | function-table dispatch, per argument index | - -**Elements are a field named `[]`.** There is no separate element dimension; -element reads and writes are ordinary reads and writes on that pseudo-name. -Typed-array *kind* is carried on the abstraction and the class instead. - -Abstractions are per-`(allocation site, context)`, plus snapshot objects, -function-object statics spaces, synthetic prototype abstractions (whose field -space *is* the class's method table), and synthesised native namespaces. A -class's identity is preferentially its concrete `.prototype` object, falling -back to its constructor script, falling back to a per-allocation-site -pseudo-class for classless literals and arrays. - -**The reader** (`read_into`) dispatches on the receiver's object part: a `One` -receiver reads its own field cell plus the class field cell plus the accessor -getter plus the prototype chain — *not* the union root; a `ClassAny` reads the -class view plus the prototype abstraction and its chain; an `AnyOf` yields -`P_UNKNOWN` except for elements, and — **at callee position only** — the -region's method-table union, capped. That last exception is why call resolution -survives polymorphism while field typing does not. Prototype chain lookup is a -monotone join up to depth 8, never an if-else. - -**The writer inventory** is the mechanism behind every field claim, and it has -three routes plus a fourth attribution channel: - -| receiver | destination | -|---|---| -| `One(a)` | that abstraction's field cell | -| `ClassAny(c)` | that class's field cell | -| `AnyOf` / `AnyObject` | **dropped** (and the value escapes), except elements, which go to the bundle-wide union | -| a function | its statics cell; `.prototype` installs a prototype source, never value flow | - -The fourth channel is what makes the inventory survive receiver saturation: -every `this.name = v` also raises into a per-script `ThisField` cell, which is -linked into the class field cell of every **home class** of that script. Homes -are installed for a constructor over its own class, for a method installed on -exactly one class, and transitively along `this`-forwarding call edges. Homes -are capped at 8 — a script homed everywhere is a shared helper, and more links -is the mega-cell again. - -Everything then converges on the class view cell, which holds the join of every -writer that reached the class by any route. That cell is the emission oracle. - -### 6.4 The engine - -Nine constraint kinds (`Move`, `Const`, `Read`, `Write`, `Call`, `Apply`, -`ElemBuiltin`, `Alloc`, `Arith`), generated once per script and evaluated per -`(constraint, context)`. Constraint identity is context-free; operands resolve -to cells at evaluation time. - -Two propagation mechanisms coexist: - -- **Subscriptions** (lazy, per reader). Reading a cell records the reader and - returns a snapshot clone. When the cell grows, every subscriber is - re-enqueued. Installed on first read, permanent. This is what makes - late-arriving evidence re-fire readers that already ran — the answer to the - one-way-edge problem that directed constraint graphs otherwise have. -- **Standing links** (eager, cell to cell). A permanent edge that immediately - propagates the current value. All the structural plumbing uses these: field - to class view, class field to class view, prototype source feeds, `this`-field - to class field, element cells to the bundle union. Link cycles terminate - because a raise stops at no-change. - -The worklist is FIFO with a membership set for deduplication. Bootstrap -instantiates every script at the generic context in sorted id order. - -**Contexts.** A context is one interned id standing for a bounded call string — -not a graph copy; nothing is cloned. `push(ctx, sid, pc, callee)` applies four -rules in order: - -1. **recursion collapse** — if the callee already appears in the parent chain, - reuse the context it was entered at, so a strongly connected component is - context-insensitive internally; -2. **depth cap** — beyond 8, fall back to the generic context; -3. **budget** — beyond 50,000 contexts, fall back to the generic context; -4. otherwise intern on `(caller context, call site, callee)`. - -A fifth degradation lives at the call site: a site with more than four -non-builtin targets binds every target at the generic context. All five are -counted and reported. - -Regions get their own context discipline: a callee resolved through a region's -method tables binds at a **depth-1 context parented at the generic context**, -one per (site, target) — never at the generic context itself (measured as -shared-row pollution, -18% on one benchmark) and never chained off the caller's -context (region fan-out through recursive callees exhausted the budget and -degraded the whole pipeline). - -**Termination** rests on: every cell only rises, through joins monotone in each -component; each component has constant height; a constraint is re-enqueued only -when a cell it read grows; contexts are finite (interned, depth-capped, -budget-capped, recursion-collapsed); the union-find only merges; and link cycles -stop at no-change. - -**Determinism** comes from dense allocation-ordered ids, sorted iteration at -bootstrap and in every emission loop, a min-root union-find, and immutable -per-abstraction join metadata. Order-independence is asserted by a shuffle test -over small graphs rather than by construction; the whole-bundle claim rests on -that diagnostic, not on a proof. - -**One narrowing is deliberately unsound as dataflow.** A polymorphic dispatch -site whose receiver was lost does *not* propagate that lost receiver into a -method with a pinned `this` class. As dataflow that is a refusal of evidence; as -a prediction it is exactly the point — the dispatch site's lost receiver must -not destroy the body's asserted precision, because the body will guard anyway. - -### 6.5 Calls - -There is no separate call-graph structure: the callee is a cell, and the -dispatch ladder reads its callable set. Callable identity enters cells from -lambda opcodes, global-name seeds, the live global object's properties, -snapshot object properties, prototype-abstraction reads, and synthesised -natives. - -At a call, arguments are read (which **subscribes**, so a later widening of a -caller operand re-fires the whole call constraint and re-binds) and raised into -the callee's per-context argument cells; the return flows back the same way. A -`this`-forwarding edge additionally records a delegation edge, which is how home -classes propagate. - -Construct is richer: the site allocates an abstraction, raises it into the -callee's `this`, and — for a constructor with an explicit return — yields the -return where it is an object, joining the allocation in only where an *observed -primitive* return path exists. `P_UNKNOWN` explicitly does not count as one, -because construct semantics box a primitive return to the object either way. - -**The function-table channel** solves a specific and common shape: a table of -functions saturates its element cell past the 8-identity cap, so dispatch -through it resolves nothing. Dispatch stays opaque — the return is unknown and -the arguments escape — but the site's *argument profiles* are raised into -per-index join rows on the table abstraction and fanned into every known -member's formals. Membership is collected where identities are still singular: -snapshot elements, per-context element writes, and per-site argument values -recorded before the row saturates. The members learn what the table is called -with even though no call site knows which member it reached. - -**Escape.** A function value reaching an untracked sink gets a distinguished -*unresolved* type joined into its generic-context formals and `this`, once per -script. The value used is deliberately `unresolved` rather than "top": a -fabricated definite primitive mask poisoned every field cell an escaped method -wrote through `this`, and was indistinguishable from real evidence. - -**Natives** are modeled from a spec table. Two refinements carry weight: an -*integral* native (`floor`, `round`, `parseInt`, ...) raises the magnitude -claim, and an *integrality-preserving* native (`pow`, `abs`, `min`, `max`) -does so only when its arguments are integral at that site — without which one -cold `Math.pow` call widened every arbitrary-precision digit cell in a -benchmark to unbounded. - -`Object.defineProperty` is the one native with modeled *heap* semantics: it -reads the descriptor's getter and setter fields, registers the accessor pair on -the target's class, seeds the accessor bodies' `this`, and re-fires same-name -heap constraints. - -### 6.6 What comes out: `LikelyFacts` - -`compiler/src/facts.rs`. Every field is a prediction that codegen re-checks. The -contract is stated at the top of the file and holds for all of them: *a wrong -fact costs a failed guard, never correctness.* - -`LikelyFacts` also carries the compilation's **one string table** (`Names`, -`ids.rs`). Every name in the tables below — property names, global bindings, -layout field names — is a `NameId` into it, and the table itself is handed on -to the translator, which takes ownership and adds the emitted module's dense -`atomId` numbering on top rather than keeping a second copy of the strings. So -a name is interned once, at the syntactic scan that precedes the analysis, and -crosses every phase boundary as an integer. - -Facts are grouped here by what they license. The recurring emission discipline -is: **a per-site fact is the join over live contexts, and a genuinely -polymorphic site emits no fact at all.** - -#### Class layout and stamping - -| fact | says | licenses | -|---|---|---| -| `class_layouts` | per class, the ordered first-write field names; **position is the predicted fixed-slot index** | every checkless fixed-slot address; the serialized layout table the C++ validator checks against the live shape | -| `class_layout_masks` | parallel per-position value masks (numeric fields only) | the constructor-init conform check, and a load that skips the value tag test on a TYPES-and-SLOTS receiver | -| `class_layout_ranges` | parallel per-position intervals, only where a mask is also claimed | the store-side prove-or-clear duty; the three-bit stamp fold; a loaded value that arrives *with an interval* | -| `class_layout_typed_masks` | the name-keyed claim filling positions the layout tier left blank | takes priority when building the per-class mask row | -| `ctor_stamps` | constructor script to its class key | **the exit stamp itself.** Without it nothing is ever stamped and every class-fact guard misses | -| `ctor_nslots` | the constructor's full row length | the `new` site's allocation size, so every predicted field lands in a fixed slot | -| `deleg_restamps` | an init delegate to the full class key it completes | the restamp at the delegate's returns, advancing a prefix key to the full key | -| `deleg_inits` | scripts homed as `this`-forwarded delegates | keeping the add-transition arm on the set-cache tail in those bodies only, where it is worth over a million fixed-slot nursery adds per run, and nowhere else, where it is bloat | -| `construct_site_keys` | for the shared-constructor idiom, the per-site class | the allocation size and stamp word where the script-keyed tables cannot key | -| `group_tables` | per predictor group, the universal prefix names and the masks every member claims | makes a *range* class fact consumable: the guard becomes a key-range compare against a shared prefix table | -| `this_layouts` | a method script's predicted `this` class, exact or a range | serving `this.f` at sites with no per-site row | - -#### Per-site property and element facts - -| fact | says | licenses | -|---|---|---| -| `prop_sites` | per site: class-key range, predicted slot, value mask | **the class-fact arm** (section 5.2 L1) on both the get and set sides | -| `typed_sites` | the name-keyed type dimension, independent of the slot dimension — including names absent from every layout, and classes whose slot prediction never validates | fills the mask where the slot tier left it blank; the difference between a two-bit guard and a tag-free typed load | -| `field_sites` | per-GetProp value claim, retained only where no fenced table already covers the site | orders the typed-load ladder on an inline-cache result | -| `elem_sites` | per-GetElem value claim | orders the typed-load ladder on a dense read | -| `array_elem_claims` | per array region root: mask, low, high | array stamp keys, and the bundle-wide intersection an unclassified element store must honour | -| `array_alloc_sites` | which allocation sites stamp a fresh array | the stamp word written at allocation (the claim holds vacuously on an empty array) | -| `array_elem_recv` | the region root at each element site | the read-side fold and the write-side duty | -| `ta_elem_sites` | a settled typed-array kind at an element site | the guarded monomorphic typed-array arm | -| `elem_poly_sites` | every element site in a bundle that mentions a typed-array constructor; **empty otherwise** | the shared polymorphic typed-array probe — and its *absence* keeps typed-array-free programs free of a cold call in hot loops | - -#### Calls, arguments, results - -| fact | says | licenses | -|---|---|---| -| `calls` | per site, up to 4 resolved callee scripts | **all splicing** (no entry means no splice); the likely-direct call arm; and it *suppresses* the builtin arm bundle, since a site predicting a script would only carry dead diamonds | -| `native_calls` | that a site settled on *some* one modeled native — not which | the flags-fork gate around a builtin arm | -| `apply_sites` | every syntactic `.apply`/`.call`-shaped site | the apply-forward lowering, which elides the `arguments` object entirely | -| `accessor_sites`, `accessor_names` | resolved accessors, and names that are accessors on any class | the accessor arm, with and without a static target | -| `arg_types` | per script, `this` and formal claims | the guard-at-definition ladder at `GetArg`, and the body's typed-entry contract | -| `call_types` | per site, the result claim | one tag test on the generic call continuation. Object claims are emitted **only under receiver demand** — the result must feed an element access, because the element lowering's receiver tag test elides against a bare object proof while a property lowering's class-word guard does not. Claiming them blanket-wide is a substantial loss | - -Every field here has an emission consumer. Nothing in `LikelyFacts` exists -purely to be printed: the un-projected lattice point, the class-region -union-find grouping and the layout-to-ctor mapping were all carried this far -for the viz layout panel alone, and were deleted with it. The panel now shows -only what the backend actually has — the stamped id, and per field its slot, -mask and range claim — which is the whole point of a panel that claims to -show what the compiler is using. - -#### The fenced hierarchy - -Property facts form a rung ladder, all speaking one guard form — a key-range -compare against the stamped class word — at four widths: - -| rung | shape | -|---|---| -| exact constructor class | `lo == hi`, per-class row and masks | -| predictor group | `lo < hi` over a group's universal prefix, masks every member claims | -| region | `lo < hi` spanning several groups that met in a union-find region | -| per-name sub-range | the longest run of contiguous keys agreeing on a slot | - -Class keys are assigned **region-contiguously** precisely so a region fact is a -plain range test in the same key space. A region range is minted only when the -region has at least two keyed groups *and* contains at least one constructor -key — a range holding only object-literal keys can never hit, and each read -pays the miss. - -The hierarchy is closed by a **subsumption rule**: the unfenced per-read value -claim is dropped at any site already served by a masked class fact, because the -overlap is pure double-guarding. ---- - -## 7. opsem: the shared vocabulary - -`compiler/src/opsem.rs`. One module holds the result-type algebra, the magnitude -rules and the exact-integer interval algebra of the JS numeric and string -operators, **written once and shared by the analysis and the codegen**. - -Everything in it is stated at one epistemic level: what the whole program -suggests, shaped as the optimistic ladder the codegen committed to. `int32 op -int32` claims int32 with overflow and `-0` as side-arm territory; integral sums -and products claim the exact-integer domain the same way one level up. Nothing -here is a proof, and every result is consumed behind a guard or a fence. - -That sharing is the point. The analysis's optimistic ladder and the codegen's -arm structure are the *same* rules, which is why an analysis claim and the arm -that guards it cannot disagree about what "int32 plus int32" means. - -### 7.1 The alphabet and the magnitude lattice - -Seven primitive bits (`INT32`, `DOUBLE`, `STRING`, `UNDEFINED`, `NULL`, -`BOOLEAN`, `BIGINT`), re-exported by both the analysis's type sets and the -codegen's contexts. Bit 14 (the analysis's unresolved-evidence marker) and bit -15 (the object-only claim) are deliberately outside the alphabet. - -`Range` is a three-point chain `I32 < I53 < Top` describing the value's -magnitude *when it is a number*: whether it is known integral and exactly -representable in 53 bits. `I32` is the bottom (no evidence of anything wider); -joins take the max. - -An operand is projected into a single view — its possible primitive classes, a -`wild` bit meaning "may be an object, a function, or unresolved evidence, so -`ToPrimitive` could surface anything", and its magnitude. Both type -representations project onto this losslessly for the modeled operators. - -### 7.2 The interval algebra - -This is where the compiler's arithmetic proofs come from. - -```rust -type Iv = Option<(i64, i64, bool)>; // lo, hi, may-be-negative-zero -``` - -A `Some` value asserts: the value is a finite integer in `[lo, hi]`, never `-0`, -with bounds within `+/-2^53`. Every such value — and every in-bounds -intermediate of the modeled operators — is an **exactly representable double**, -so `f64` evaluation of those operators is bit-exact integer arithmetic. `None` -means no proof. - -Two properties make this a *proof* rather than a prediction: - -- Every value carrying an interval traces to canonically-boxed producers: - constants, bit-operation results, int32-tag-guarded seeds, and the compiler's - own arithmetic. So an interval within int32 range additionally proves an - **int32 tag**, under the engine's canonical-boxing invariant. -- The third component tracks `-0` precisely rather than conservatively: a - product is `-0` only when one factor is 0 and the other negative; a sum is - `-0` only when *both* addends may be; bit operations and shifts **cleanse** - it, since `ToInt32(-0)` is `0`. Only *clean* intervals are recorded as facts, - but a flagged intermediate may still ride an operand so that a downstream - operation can cleanse it. - -The transfer rules: - -| operator | rule | -|---|---| -| `+` | `[al+bl, ah+bh]`; flagged only if both addends may be `-0` | -| `-` | `[al-bh, ah-bl]`; flag from the left operand only | -| `*` | min/max over the four corner products with checked multiplication; flagged when one factor straddles 0 and the other is negative | -| `%` | defined only for a provably non-negative, unflagged dividend and a divisor range excluding zero; result `[0, min(|b|max - 1, ah)]` | -| unary `-` | `[-ah, -al]`, flagged when 0 is in range | -| `&` | an int32 AND-ed with a provably non-negative side is `[0, that side's hi]` | -| `\|`, `^` | two non-negative int32s set no bit above the higher operand's leading bit | -| `<<`, `>>` | exact for a *constant* shift; otherwise full int32 | -| `>>>` | always `[0, 2^32)`; exact for a non-negative int32 left operand and a constant shift | -| `~` | `[-ah-1, -al-1]` | -| `/`, `**` | no rule — division is never exact-integer in any rendition | - -Two **finite-height devices** sit on top, and it is worth keeping them apart -because they solve different problems: - -- **Context widening** (used by the codegen's fixpoint). A slot's interval keeps - the *exact* union for its first three growths — which is what lets a - self-bounded loop accumulator converge to its true range instead of snapping - past it — and only then climbs a rung ladder `0 / +/-2^31 / +/-2^36 / - +/-2^48 / give up`. The intermediate rungs are load-bearing: real - arbitrary-precision carry chains stabilise near `2^35`, and a ladder that - jumps straight to `2^53` pushes the dependent sums out of the domain before - the fixpoint can settle. -- **Heap quantization** (used by the analysis). Bounds are rounded outward to - the next power of two (magnitudes up to 8 stay exact) at *construction*, so - the heap interval join is a plain hull — exactly commutative, associative and - order-independent. The ladder is clipped to the int32 domain and collapses in - one step beyond it: a wider bound serves no consumer, wide intervals breed - unboundedness through products anyway, and climbing twenty more octaves of - feedback measured 2-3x the solve time on one benchmark. Masked and shifted - chains recover a bound from the unbounded operand through the bit-operation - rules, so nothing real is lost. - -The distinction to keep: **quantization exists for finite height, not for -order-independence** (the hull join is order-independent for any fixed inputs), -which is why a runtime check may contribute an *exact* non-ladder bound. - -### 7.3 Who uses it - -**The analysis** projects each type set into the operand view and calls the -transfer function at the `Likely` stance for masks and magnitudes, and the -interval algebra (quantized) for values. Constant folding at scan time runs the -same algebra *unquantized*, so that a literal mask like `(1 << 28) - 1` reaches -a later `&` site with its exact bound. - -**The codegen** uses the interval algebra directly as its proof engine — there -is no separate range analysis. Intervals are one dimension of the per-op BBV -context walk: they are minted by literals, by an operand's own representation, -by a passed fits-int32 check, by a passed tag guard, and by a typed-array -element kind (a proof, since the class guard pins the kind); they are propagated -by the transfer rules at every arithmetic emitter; they are joined with widening -at every merge; and they are cashed in at three places — overflow and `-0` check -elision, the narrowing of a mixed numeric mask to exactly int32 (the interval -proves the *tag*), and the prove-or-clear obligations that maintain heap range -claims. - -Predictions are promoted to proofs in exactly one place: the int32 arm of a tag -dispatch seeds the analysis's predicted range as an interval, licensed by the -tag test that was just passed. - -A second, *proven* stance beside this one -- for consumers that need a claim -true on every non-throwing path -- does not exist: the codegen derives its -proofs from the interval algebra instead. A future consumer needing that -stronger guarantee would add it as a second instantiation of these same -rules. ---- - -## 8. The runtime and the ABI - -`js/src/night/runtime/`, C++, linked into `libjs` so that the wasm shell carries -it. It is the only SpiderMonkey surface generated code touches directly. - -### 8.1 The helper ABI - -`NightRuntime.h` is the single ABI header: flat, POD-only C, `extern "C"`, no -C++ types crossing the boundary. Each entry point is preceded by an export -macro that expands on wasm to `__attribute__((export_name(...), used))` and to -nothing natively — so the same runtime builds natively for unit tests. - -Calling conventions: - -- Every **may-GC** helper takes `top` as its *second* parameter: the GC scan - limit, installed on entry. `top` doubles as the scratch out-slot — a helper - with a boxed result writes through it, into a slot that sits exactly at the - scan boundary and is therefore outside the rooted region. -- **Leaf** helpers, which may neither GC nor throw, omit `top`. Each such - declaration carries an explicit justification (for example, the global-slot - resolver uses a pure, non-allocating lookup). -- Helpers that can throw return a boolean with the result in an out-parameter. - The two cache-miss helpers instead return a two-bit code, described below. - -**The helper list is an X-macro**, and its signature strings are *derived from -the real C++ types* by a constexpr template that maps each parameter and result -type to a wasm letter. That is the load-bearing design property of the ABI: -signature drift between the C++ declaration and the wasm import is made -structurally impossible rather than asserted. There are 133 helpers. - -| family | ~count | representative | -|---|---|---| -| calls and construction | 8 | `call`, `native_dispatch`, `apply_fwd`, `construct`, `create_this`, `callee_night_target` | -| property access and cache misses | 8 | `get_property`, `get_prop_ic_miss`, `set_prop_ic_miss`, `get_element`, `set_element` | -| object and array literal init | 7 | `new_object`, `new_array`, `init_prop`, `init_elem` | -| globals, names, bindings | 17 | `get_gname`, `resolve_global_slot_guarded`, `bind_name`, `get_intrinsic_cell` | -| environments and closures | 12 | `env_setup`, `get_aliased`, `push_lexical_env`, `enter_with`, `lambda` | -| arithmetic, compare, string, conversion | 19 | `add`, `binop`, `compare`, `math_unary`, `fmod`, `str_chars_eq` | -| exceptions and spec checks | 13 | `throw`, `exception`, and nine `check_*` helpers | -| generators and async | 12 | `create_generator`, `gen_suspend`, `async_await` | -| iteration | 8 | `iter`, `more_iter`, `close_iter_for_exception` | -| `super`, home object, prototype | 7 | `super_base`, `get_prop_super`, `mutate_proto` | -| `arguments` | 5 | `arguments`, `get_mapped_arg` | -| GC write barriers | 3 | `post_write_barrier`, `post_write_barrier_elem`, `pre_write_barrier` | -| misc, builtins, diagnostics | 8 | `instanceof`, `regexp`, `builtin_object` | - -The generator and async family is live: the suspend/resume state machine is -lowered in `bbv/generator.rs`, and the two leaf closing checks -(`gen_closing`, `gen_is_closing`) are what the error epilogue and the -catch-pad split call. - -**How helpers are bound** differs by flow and is the one real asymmetry between -them. In-process, they are wasm **imports** bound to host function pointers -(and on `wasm32` a C function pointer *is* an indirect-table index, which is -exactly what import resolution needs). In the snapshot flow, `resolve_helpers` -looks each one up as an **export of the module being rewritten** — the runtime -is already inside the image. Both produce the same handle struct for the -translator. - -### 8.2 The value stack - -`NightStack` is one contiguous, upward-growing array of boxed `JS::Value`, and -it is **the sole GC root for object references held by compiled code**. AOT -frames — callee, `this`, formals, locals, spilled operands, the LICM hoist -region — live here; not in the GC heap, and not in wasm locals. - -It is a 2 MiB allocation owned by the runtime, present from runtime -construction with no registration step. Its size is fixed: a frame that would -not fit causes the entry point to decline and the interpreter to run the script +# NightMonkey design + +NightMonkey is an ahead-of-time compiler from SpiderMonkey bytecode to +WebAssembly. SpiderMonkey and compiled JavaScript execute in the same Wasm +module and linear memory, so compiled bodies can call engine helpers, inspect +engine data structures, and call one another. + +This document describes the current implementation. It separates correctness +contracts from policy: limits and predictions may reduce coverage or +performance, but must never be required for correct execution. + +## 1. Scope and deployment contract + +NightMonkey consumes a closed snapshot of scripts and selected heap objects. +It does not consume execution profiles. The intended deployment is one +wasm32-wasi SpiderMonkey runtime, one active `JSContext`, and one installed AOT +environment per process. + +Those restrictions are currently embedding assumptions, not fully enforced API +properties. Most AOT runtime state is process-global. An embedding must not +install two environments or activate NightMonkey in two runtimes. The +module-wide BigInt optimization also assumes values cannot enter from source or +an embedder outside the captured program. Until enforced or removed, these are +part of the trusted deployment boundary. + +A compilation unit is a registered script tree plus selected self-hosted +scripts and regular-expression programs. A script is compiled as a whole or +remains interpreted. Unsupported operations, environments, limits, or +translation failures never produce partially compiled scripts. + +## 2. End-to-end flow + +Two deployment modes use the same analysis and translator. + +### Snapshot flow + +1. The wasm32-wasi shell compiles and registers script roots. +2. Registration delazifies reachable function trees and serializes information + an external memory reader cannot safely derive. +3. Wizer captures the initialized module. +4. The host `nightmonkey` tool reads the registration block and snapshot, + constructs `Source`, runs analysis, appends compiled functions, lays out AOT + data, and patches function indices and object stamps. +5. On resume, `JS::NightActivate` installs the environment and enables AOT + dispatch. + +### In-process flow + +1. The wasm shell captures live scripts and heap objects into `Source`. +2. The NightMonkey compiler, inside the Wasm instance, builds a temporary + module with imported runtime helpers and serializes its defined functions as + runner blobs. +3. `wasm-jit-runner` injects the functions into the running instance. +4. The shell installs the environment descriptor and patches scripts to their + injected table entries. + +The helper indices, table indices, region addresses, and serialized tables must +describe exactly the module that receives the blobs. + +## 3. Compiler inputs and identities + +`compiler/src/source.rs` owns the compiler input. `SourceObject` variants +describe scripts, scopes, objects, strings, symbols, and primitive values. +`source/ffi.rs` builds the graph in-process; the snapshot crate builds it from +the captured image. + +Typed identifiers in `ids.rs` distinguish scripts, bytecode PCs, program +sites, names, layout keys, and their biased runtime stamp keys. Traversal of +maps that affects emitted identities must be stable; diagnostics and hash-table +iteration must not influence output. + +`bytecode.rs` uses a generated `JSOp` enum and generated operand and stack +metadata. Its parser supports direct decoding and the `OpcodeVisitor` interface +used by analysis and prepasses. + +## 4. Likely-facts analysis + +`likelier` is an optimistic whole-program dataflow analysis. Its results are +predictions, not proofs. Codegen may use them only to select and order guarded +paths; a failed guard must reach a semantically complete path. + +### Constraint graph and fixpoint + +`scan.rs` walks each script once and builds constraints over an abstract +operand stack. Locals are flow-sensitive. Assignments create new abstract +values, control-flow joins create explicit join values, and loop headers +materialize joins eagerly for back edges. + +`engine.rs` owns cells, constraints, subscriptions, provenance, and the +incremental worklist. Constraints are generated once and evaluated in each live +calling context. Cell growth requeues its subscribers. + +`types.rs` combines bounded primitive and function sets, numeric magnitude and +interval information, and an object abstraction that widens from one allocation +to a class and then arbitrary object. Joins only weaken information. Every +resource limit must degrade in that direction: merge contexts, mark overflow, +drop a heap prediction, or widen to unknown. + +### Calls and heap + +`calls.rs` represents context as an interned bounded call string. Arguments +and receivers flow into context-indexed callee cells; returns flow back to call +sites. Recursion, depth, fanout, and global budget limits fall back to the +generic context. + +`heap.rs` models snapshot objects, allocation abstractions, fields, prototypes, +arrays, and constructor classes. Snapshot state seeds the same cells that later +program writes update. Property reads join every prototype level they might +observe. Elements use a separate abstract field to avoid merging unrelated +prototype elements. + +The points-to model of the heap is a combination of Andersen points-to (with +points-to sets and membership/subset relations) and Steensgaard points-to (with +a union-find data structure that merges abstractions bidirectionally on any +interaction). Specifically, a typeset describes its object-reference component +as an element in a lattice that contains allocation sites at the bottom tier; +then constructor classes (every allocation site belongs to one constructor +class or a special pseudoclass for object literals); then union-find "regions" +of classes. Flow is still directional (no bidirectional union as in +Steensgaard), but where an Andersen points-to set would grow from one to +multiple elements, our points-to model instead merges the merging classes in +the union-find and represents the merged value by pointing to that region instead. -`[base, top)` is live and rooted. Its tracer walks exactly that range calling -the root tracer per slot, which forwards moved pointers **in place** and -no-ops on non-GC values. It is called from the context's trace hook on **every** -GC, minor and major — deliberately, because an embedding's extra-roots tracer is -major-GC-only and would leave nursery pointers in AOT frames stale. - -Native code re-entering a compiled body wraps the entry in a scope guard that -saves and restores `top`. Direct compiled-to-compiled wasm calls pass `sp` -explicitly and skip the guard; `top` self-corrects at the callee's next may-GC -point. - -### 8.3 Dispatch - -Three entry points, all consulted from exactly three places in the interpreter -and **always after the JIT has declined**: `RunScript`, the interpreter's inline -call fast path, and generator resume. - -Two runtime gates decide compiled versus interpreted: the script's AOT function -index is nonzero, and the tier has been activated. A third gate is compile-time -— the script was compiled at all. - -The entry ABI is the same signature described in section 5.5, and the AOT -function index **is** the C function pointer: LLVM lowers it to an indirect -function table index, so calling it becomes a `call_indirect` of exactly that -signature. The caller stages `[callee, this, formal0..N-1]` on the value stack, -padding missing actuals with `undefined` because the body reads formals -positionally. A global body gets `[undefined, globalThis]` and zero arguments. - -Return is an `i32` error code with the boxed result written through the -out-parameter. Construct semantics — substituting `this` for a non-object return -— are applied by the *caller*, because a compiled body has no interpreter frame -epilogue. - -**There is no on-stack replacement and no loop side entry from the -interpreter.** Entry is only ever at a function's first op. The one thing that -resembles a side entry is generator resume, which is not OSR: it stages a -sentinel `this` and a resume descriptor above the published stack top -(unscanned, and consumed by the body before any GC can happen), and the body's -own entry dispatcher restores state. Entry is still at the physical entry -block; the dispatcher is the first thing it forks to. Because a suspended -compiled generator's saved storage layout is the AOT tier's own, such a -generator cannot fall back to the interpreter — which is why -`IsNightResumable` gates the interpreter's `JSOp::Resume` into -`EnterNightResume`. - -Going the other way, compiled code calls the flat helper ABI. Generic -call-out re-enters the engine's call path; specialized sites first call a leaf -classifier that returns the callee's script and function index packed in a -64-bit word, then `call_indirect` straight into the callee, bypassing the engine -entirely. - -### 8.4 GC and rooting - -**Rule zero: nothing raw survives a may-GC point.** A raw JS object pointer is -never held across a call in a wasm local or in linear memory. The only durable -representation is a boxed value in a stack slot below `top`, which the tracer -forwards in place. - -The **spill/reload handshake** is mechanical. Before a may-GC call: box every -live operand and store it into the frame's operand region, set `top` past them, -call. After: reload every spilled slot (the objects may have moved) and reset -each operand's representation to boxed. An error return routes to the enclosing -exception handler. - -Locals are always boxed in the frame, always below `top`, hence always scanned; -the SSA carrier on top of them is a *cache*, and stores are write-through so -the frame stays the rooted truth. At every may-GC point the carrier sweep drops -exactly what a GC could invalidate: raw pointer representations always die, a -boxed carrier survives only if its fact proves it can never hold a GC thing, and -raw numeric representations are immune. - -**Nothing in the reserved memory regions is traced.** The cache regions hold raw -shape, holder, prototype and callee pointers with no generation field, and are -instead **zeroed wholesale on major GC** by a registered callback. The reason is -compaction plus address reuse: a freed-then-reused shape address would -*false-hit* an otherwise sound guard. The callback runs at **both** GC begin and -GC end, and the two-sided rationale is worth keeping: at begin, so that no -pre-GC entry survives into an inter-slice mutator window of an incremental GC -(a sweep slice can free a cached-but-dead shape whose address the mutator then -reuses); at end, so that entries refilled mid-GC and then moved by a compacting -phase are also dropped. - -Two regions are deliberately *not* zeroed, because they carry a per-cell -generation stamp and their hit paths re-read live state rather than trusting a -cached pointer; two more hold static tables that must survive. A second callback -handles minor-GC end: re-arm identity cells whose value was nursery-young when -resolved, and — only if some row actually cached a nursery prototype — zero the -add-transition rows. - -The **helper-author contract**, as documented in the ABI header: every may-GC -helper takes and installs `top`, and writes any boxed result through it; only -helpers explicitly identified as leaves may omit it, and a leaf must neither GC -nor throw. - -**Write barriers are inlined with their gate; the helper is only on the slow -edge.** The post-write (generational) barrier emits the raw store plus an -owner-tenured test, an is-GC-thing tag test, and a value-in-nursery test, all -inline, calling a leaf helper only when all three pass. It is elided entirely -when the stored value's type proves it holds no GC pointer. The pre-write -(incremental) barrier reads the zone's needs-barrier flag inline and marks the -old value only during active marking. Barrier leaves never move anything, so -they need no rooting handshake. - -The engine offsets those inline barriers and the inline element access bake in -are **release-asserted at startup**: the zone and realm offsets, the elements -header offsets, the frozen flag, the function's script slot, the AOT index -field, and the object header offsets the stamp mechanism depends on. That is -the right discipline, and section 8.5 explains why its absence elsewhere is a -hazard. - -### 8.5 The reserved linear-memory regions - -`layout_env` (`compiler/src/wasm/mod.rs`) computes one `EnvLayout` whose bases -are **baked as absolute `i32` addresses** into every compiled body. There are -two allocation phases: a fixed-size block laid out before translation (sized -from analysis outputs the translator needs to bake addresses for), and a -post-translation block whose sizes are translation outputs — for those, bodies -bake placeholder constants that a patch pass rewrites once the sizes are known. - -The base address differs by flow: the snapshot image's current memory end, or a -zero-filled arena allocated at startup. - -| region | purpose | sized by | zeroed on major GC | +Each heap abstraction carries predicted types per field name and predicted slot +number (property/shape order) per field name. + +Constructor events record ordered writes and delegation. `likelier/emit.rs` +forms predicted fixed-slot layouts, groups compatible prefixes, assigns stable +layout keys, and emits site facts. `likelier/effects.rs` computes post-fixpoint +effect summaries per function (script). + +`facts.rs::LikelyFacts` is the analysis-to-codegen contract. It contains value +claims, call resolutions, heap and element predictions, effect summaries, +global facts, and inlining inputs. Codegen should not reach into solver state. + +`opsem.rs` is shared vocabulary for primitive sets, numeric magnitude, +intervals, and modeled operator results. It prevents analysis and codegen from +maintaining separate arithmetic semantics. Predicted results remain untrusted; +an interval originating at a guarded or canonical producer can become a proof +inside codegen. + +## 5. Translation and basic-block versioning + +`wasm/mod.rs` runs analysis, lays out memory regions, translates scripts and +regexps, places functions in the indirect table, and patches address +placeholders after bases are known. + +`wasm/bbv` is the actual JavaScript bytecode to Wasm bytecode translator. Each +bytecode operation ends a generated block; a workqueue emits every reachable +structural version. + +### Versions, predictions, and tracks + +The overall strategy of the codegen backend is to emit code in two "tracks": +optimistic (OPT) and generic (GEN). The optimistic track is meant to align with +all of the predictions that the static type analysis makes; if the program +diverges from those types, execution is shunted to the generic track. Likewise, +in the other direction, if execution in the generic track can prove that it +meets all the assumptions of the optimistic track, we can shunt execution back. +We sometimes call these "offramps" and "onramps", colloquially. Each transition +may involve some boxing/unboxing, because OPT can carry values in raw unboxed +form. + +Reducibility concerns (Wasm requires reducible CFGs) complexify the two-track +design somewhat: we need to duplicate code further into versions that are keyed +on which loop headers they are dominated by. Otherwise, an onramp or offramp +would become a side-entrance to the other copy of any loop in the current +loop-nest. + +A basic block in the emitted IR is part of the lowering for a given JSOp for +one "version". That version is identified by its PC, execution track, +nested-loop token-vector class (the above-mentioned reducibility scheme), and +inline-segment depth (the means of conceptually duplicating code for inlining). + +Each version carries a fact context: known types, object class-stamps (see +below), and unboxed representation choices for each value. `predict.rs` +computes one optimistic context per program point; the generic track carries no +speculative facts. + +The translator first runs a context-only fixpoint, then emits against the +closed prediction map. If emission discovers an unclosed successor, the script +is retried with less specialization. The bottom compile-ladder rung emits +generic-only code. + +The optimistic track carries facts proved by guards and prior operations. Side +arms handle cases outside an optimistic lowering and continue with weakened +facts. The generic track uses boxed values and runtime helpers and is the +correctness floor. + +All inter-operation edges pass through the continuation and `theta` machinery +in `version.rs` (named for the corresponding `theta` function in Static Basic +Block Versioning, which our version management previously followed more +closely). This code owns fact joining, track weakening, loop tokens, reducible +CFG construction, values carried across blocks, and guarded recovery from weak +loop or call-return paths. Lowerings must not bypass it for ordinary bytecode +successors. + +### Representations and proofs + +An operand records Wasm representation separately from JavaScript type. Common +representations are boxed `JS::Value`, `i32`, exact integer `i64`, `f64`, object +or string pointer, and boolean. + +A codegen fact must originate in a dominating runtime guard, an exact producer, +a helper or canonical-boxing invariant, or preservation across a proven effect +class. An analysis prediction alone may not manufacture an unboxed value or +remove a required check. Generic helpers accept and return full boxed values +and preserve JavaScript exception behavior. + +Before a may-GC call, every live GC value must be visible in the traced AOT +stack or another engine root. `live.rs` and frame flushing determine what is +materialized. Compiled direct calls return an error result and effect bits; +effect bits kill heap, stamp, and binding facts but do not replace rooting. + +### Inlining + +Inlining creates a synthetic bytecode segment in the caller's PC space. The +callee uses an alternate frame view and its returns rejoin the caller. Try +notes, environments, arguments, script-relative operands, generator state, and +absolute side-table PCs need special handling. + +The implementation currently admits a callee unless an opcode is in the manual +`splice_blocked` list. That list is a correctness boundary: omitting a +root-frame-relative or script-relative lowering can miscompile. The desired +invariant is complete use of active frame/script abstractions so ordinary +lowerings are splice-safe by construction; we will eventually complete that +migration. + +## 6. Object layouts and stamps + +At runtime, all objects are "stamped" with their class ID according to this +lattice, and have bits indicating whether they still conform to the predicted +types and slot numbers. Emitted specialized code can guard on these ID-stamps +and validity bits to enable the use of raw, unchecked property accesses. +Properties are still always stored in boxed form, for compatibility with the +rest of the runtime. + +A 32-bit stamp occupies the wasm32 `JSObject` alignment word at offset 4. Zero +means unstamped. The low 16 bits identify a predicted layout. Upper bits say +which subclaims remain valid. During construction, identity is unpublished and +an early key identifies the layout being built. + +| Claim | Meaning | Use | Invalidation | |---|---|---|---| -| global-binding slot rows | per binding: resolved slot entry and shape | binding count | yes | -| global value-fuse cells | baked-constant global reads: value bits plus a fuse word | binding count | yes | -| cache generation word | bumped every major GC; stamps generation-guarded cells | 4 B | (bumped) | -| AOT stack limit slot | the value the call-entry guard compares against | 4 B | no (static) | -| host-constant slots | function class pointers, the unit-string table, nursery cursor and end, boxed originals of the string char methods, fuse word addresses, the Array class pointer | fixed | partly re-armed | -| builtin identity cells | boxed bits of each pristine builtin | 24 cells | re-armed | -| typed-array class table | the fixed-length typed-array class pointers by kind | 9 + pad | no (static) | -| arguments metadata | mapped/unmapped class pointers and the data offset | 16 B | no (static) | -| string-literal block | the empty string plus replay triples | 32 B | partly | -| per-layout add-check bounds | the static bound the add check consults | layout count | **no, deliberately** | -| gname fuse words | one per fused constant global | fused count | no | -| megamorphic get table | 8192 x 24 B, direct-mapped | fixed | yes | -| megamorphic set table | 8192 x 16 B | fixed | yes | -| Math native slots | `JSNative` addresses, clone-proof callee matching | 16 | no | -| dense-append cache | 512 x 32 B, shape-hashed | fixed | yes | -| accessor-call cache | 2048 x 32 B | fixed | yes | -| per-site property cache | one 20 B way plus a 48 B add-transition row, 68 B stride | site count | yes | -| callee value cells | 16 B per call site, plus a shared trash row | site count | yes | -| inline-allocation cells | 32 B per literal site | site count | yes | -| `instanceof` cells | 16 B per site | site count | no (generation-stamped) | -| construct-`this` cells | 40 B per specialized `new` site | site count | no (generation-stamped) | -| intrinsic value cells | 8 B per distinct intrinsic name | name count | yes | -| constructor slot-count table | per function index: `this` slot count and stamp key | table size | no (static) | -| serialized side tables | atom, binding, layout, fuse and regex tables | content | no (static) | - -#### The mirroring hazard, stated plainly - -**The region layout is mirrored by hand across Rust and C++, and the -synchronisation mechanism covers only part of it.** - -What *is* synchronised: the roughly 28 region **base addresses and lengths** -travel as a serialized descriptor — a word array written by the compiler and -decoded on the C++ side. In the snapshot flow that array is `memcpy`'d into the -descriptor struct behind a `static_assert` that the two are the same *size*; in -the in-process flow it is decoded positionally, field by field, with only a -length floor as a guard. - -That mechanism catches a size change. It does **not** catch a reordering, and -one field in the descriptor is explicitly a dead slot kept only so the `memcpy` -stays aligned — which is the clearest available evidence that this is a -positional wire format, not a struct. - -Every *intra-region* offset (the host-constant slots, the typed-array class -table's position, the arguments metadata block, the string-literal slot; two -of the region bases are not even in the descriptor, and C++ recomputes them -from another base) and every *entry shape and table size* (the cache way -count and stride, both megamorphic table sizes, the append and accessor cache -row counts, the builtin cell count) must agree between the compiler and the -runtime. A mismatch there is caught by nothing at the point of use: the -compiled body would index with one stride and the runtime populate with -another. That is a silent miscompile class, and it is the one place in this -system where the "guards make it safe" argument does not apply, because the -guard itself would be reading the wrong address. - -`runtime/NightRegionShape.h` holds `NIGHT_REGION_SHAPE`, an X-macro of every -one of those constants. C++ takes `Night_*` constants from it and -`static_assert`s its derived forms against them (the IC stride is ways × -way-bytes plus the transition row; `sizeof(MegaGetEntry)` is the entry size); -the three block bases C++ recomputes off `propicGenPtr` are `constexpr` -functions in that header, so the arithmetic exists once. `compiler/build.rs` -parses the same macro into `crate::region_shape`, and `translate.rs`, -`wasm/mod.rs` and `bbv/abi.rs` read the generated constants instead of their -own literals. - -What remains: the region **descriptor** is still a positional wire format -(`NIGHT_ENV_REGIONS` keeps the two sides' field lists in step by generation, -but the wire is ordered words), so a change to the shape list is an ABI -change and wants a `NightAotAbiVersion` bump. The header says so. - -### 8.6 Registration and snapshot capture - -`NightRegistration.cpp` owns the in-memory contract between an engine embedded -in a wasm module and the external transform tool. It defines the singleton -registration block, which the tool locates through an exported -constant-returning function; builds the **layout descriptor** (every engine -field offset and flag constant the external reader will dereference) by -expanding the X-macro; eagerly delazifies a registered root's whole function -tree; and builds a **digest** of facts a raw cell read cannot soundly derive — -per-script gcthing trace kinds and per-scope binding lists. Compacting GC -stays on: `NightSealSnapshotAddresses` re-derives every recorded address from -the rooted copies once, after the last GC before the snapshot is sealed, so -the addresses it records are already post-compaction. At run time raw GC -addresses live only in slots the AOT stack traces, and the linear-memory -caches that hold raw cell addresses are purged around compacting slices -(`NightPurgeMovableCaches`). - -Its resume-side counterpart installs the environment and then applies fuse -policy: if *any* script stayed interpreted, all global value fuses are -distrusted permanently, because interpreted global writes bypass the compiled -fuse hooks. - -`NightSnapshotExtras.cpp` captures what the analysis needs but the reader cannot -recover from a raw image: the irregexp **bytecode of every regex literal**, -force-compiled in both subject encodings; a **self-hosted allowlist** (22 named -builtins) resolved against the live global and delazified; and the **heap -oracle** — a full GC to tenure everything, then a transcription of the live -post-setup object graph reachable from the global and from script gcthings (own -data properties, dense elements, prototype links) plus per-scope environment -slot values read out of live call objects. That last part is what lets the -analysis see closure-captured state at all. Every entry records the object's -class pointer, so a freed-and-reused address re-reads as opaque. ---- - -## 9. The two flows - -One compiler crate consumes one input — a `Source` object graph — and emits -waffle function bodies. Two drivers wrap it. The divergence is entirely in *how -the `Source` is obtained* and *how the emitted functions become callable code*. - -### 9.1 The snapshot flow - -This is the shipping flow. - -``` -program.js - | the wasm shell reads it on stdin during wizer's init phase - v -wizer --init-func wizer.initialize -r _start=wizer.resume - | instantiates the shell, runs init, snapshots linear memory and globals - | back into the module's data segments - v -snap.wasm (a SpiderMonkey-in-wasm image with the program's heap in it) - | - | nightmonkey snap.wasm -o out.wasm (a host-native binary) - v -out.wasm (the SAME module, plus compiled bodies, plus a rewritten - | memory image) - | optionally: wasmtime compile - v -wasmtime run -``` - -During wizer's init phase the shell forces **full parse** (no lazy functions) -and registers the top-level script. Registration delazifies the whole reachable -function tree, records the layout descriptor and digest, captures the regex -programs and the self-hosted allowlist, and — after the top level executes — -transcribes the live heap (section 8.6). - -The transform tool then, in order: parses the module; flattens all active data -segments into one memory image; locates the registration block through an -exported address function; reads it (checking the ABI version); **walks** the -image into a `Source`; lays out the reserved regions starting at the current -image end; resolves the ~140 helpers as exports *of this same module*; compiles -every script; serializes the side tables and appends them to the image; stamps -each compiled script's AOT function index **into the memory image**; writes the -region table and sets the compiled flag; re-derives data segments from the -mutated image; and serializes. - -The produced artifact is **a plain `.wasm` module** — not a `.cwasm`. Running it -under `wasmtime` is a separate step, and precompiling it with `wasmtime compile` -is an optional one. - -Two mechanics are worth pinning down because they are easy to get wrong from the -outside: - -- **Nothing is "linked" in the ordinary sense.** Calls into the runtime are - direct calls to the module's own exports; calls into compiled JS go through - the C function-pointer table, and the AOT function index *is* a table index. -- **The walker reads raw linear memory**, reconstructing objects, scripts, - strings and scopes from cell layouts described by the generated layout - descriptor. Trace kinds come from the digest rather than from memory, because - they cannot be soundly derived from a cell read. Objects the heap oracle does - not cover stay opaque. - -### 9.2 The in-process flow - -This is the test vehicle and the debugging lane. The compiler crate is -additionally compiled for `wasm32-wasi` and **linked into the shell itself**, so -one `js` invocation compiles and runs its own script — no external tool, no -wizer. - -The host is `wasm-jit-runner`, a small wasmtime CLI with one trick: a host -import that lets the running guest **add new wasm functions to itself and call -them**. At startup it rewrites the guest module so every memory, table and -global is exported under a synthetic name, and strips every table maximum so the -function-pointer table can grow. Compiled modules are content-addressed and -cached. - -Three host calls: query the current table size, and two forms of "add these -function blobs". Appended functions are contiguous, so the guest can *predict* -that blob *i* lands at `size + i` — an explicit API guarantee, and the driver -verifies it after the fact. - -The guest side registers its root exactly as the snapshot flow does, walks its -**own live heap** with the same walker (over raw pointers instead of image -bytes), builds a batch, injects the blobs, copies the string-literal blob into -its reserved region, installs the environment, and stamps each compiled script's -AOT index on the live script object. - -On the compiler side the differences are contained: the module is built fresh -rather than mutated, helpers are **imports** rather than exports, the funcref -table is pre-padded so blob indices are predictable, region memory comes from a -caller-supplied allocator called exactly twice, and after translation the module -is serialized and **carved** into per-function blobs in the runner's format. -Structural index assertions guard the whole scheme. - -Two policy differences are worth knowing: - -- Global value fuses are **always** distrusted in this lane, because there is - always interpreter coverage. -- Every gate is per-script, as in the snapshot lane: a generator or async - body does not decline the whole batch. Frame-introspection tests that need - to skip AOT compilation opt out with `skip-if: nightTierEnabled()` instead. - -Compile time is inside the measured run in this lane, so it is not -perf-comparable with the snapshot lane on short workloads. - -### 9.3 What is shared - -Byte-for-byte the same code in both flows: the `Source` graph and its FFI -builders; the bytecode model and the generated opcode enum; the whole `likelier` -analysis; `layout_env`; the helper-resolution shape; **`bbv::translate_script`, -the codegen**; the whole-tree compile loop; the regex compiler; every table -serializer; **the heap walker** (only its memory accessor differs); the C++ -runtime; and registration. - -Divergent: where the compiler runs, how memory is read, whether the module is -mutated or built, how helpers are bound, where region memory comes from, how -bodies become callable, where the AOT index is stamped, the environment -descriptor's shape, the fuse policy, the generator/async granularity, and -whether anything is persisted. - -### 9.4 Build integration - -The runtime and the host binary are ordinary build directories; the host binary -is a workspace Rust program built for the *build host*, while the shell targets -`wasm32-wasi`. The in-process lane additionally adds the compiler crate as a -feature of the shell's Rust library, which is what forces its FFI symbols to be -linked. The test runner is a deliberately out-of-workspace cargo project — its -wasmtime-class dependency tree stays out of the main workspace — driven by a -forced build step, with cargo owning incrementality. - -Two build scripts generate code, and they are the model for cross-language -safety in this project: - -- the compiler's `build.rs` parses SpiderMonkey's `Opcodes.h` directly and emits - the `JSOp` enum with per-op lengths and stack effects; -- the snapshot crate's `build.rs` parses the layout X-macro out of the runtime - header and emits the Rust field enum and ABI version, with a rerun trigger, so - the Rust mirror cannot drift from the C++ header. - ---- - -## 10. The regex compiler - -`compiler/src/wasm/regex.rs`. It translates **irregexp bytecode** — the -interpreter ISA, force-compiled by the engine at AOT time — into wasm matchers. -One bytecode program becomes one standalone wasm function equivalent to a single -`RawMatch` activation: one match attempt, on a flat subject already in linear -memory. The engine keeps the global-match loop, string flattening and interrupt -handling. - -Everything is compiled **twice**, once per subject encoding, so character-load -width is a compile-time constant. - -**The signature** is six `i32` parameters (subject pointer, length, start -position, output register pointer, backtrack stack base and capacity) returning -a status: failure, success, or **retry**. Retry means "I gave up; run the -interpreter", and it is the fallback channel for everything the matcher will not -do. - -**There is no register array in memory.** The whole machine state — current -position, current character, stack pointer, backtrack count, and every irregexp -register — is an SSA vector threaded as block parameters, which waffle's -localifier turns into wasm locals. That is why the register count is capped: -every leader block carries one parameter per register. - -**It is not a dispatch loop.** A first pass linearly decodes the bytecode with a -fixed length table, collects *leaders* (offset 0 and every branch target), -validates every label, and assigns dense ids to backtrack targets. A second pass -walks the instructions in order emitting one block per leader, with every -conditional check lowered to a `CondBr` whose taken edge targets the named -leader carrying the whole state vector. - -**Backtracking** is the interesting part. The backtrack stack is a -caller-provided `i32` buffer; the stack pointer is an SSA state slot. irregexp -pushes *code offsets*, which wasm cannot branch to. The solution: - -1. a backtrack push pushes a **dense label id**, not a byte offset; -2. a pop branches unconditionally to a lazily created **dispatcher block**, - passing the id and the state; -3. the dispatcher is terminated with a `Select`, which waffle lowers to a - `br_table`, each target re-supplying the whole state vector; -4. the "push current position" and "push register" opcodes push values - *untouched*, so the save/restore of the stack pointer into a register is - oblivious to the id substitution. - -**Character classes** are baked into the code: a bit table becomes two `i64` -constants tested branch-free with a shift and a mask-select. No memory table, no -branch. - -**Fallbacks** are of two kinds. *Compile-time* bails drop that encoding's -variant (bytecode too large or misaligned, too many registers, an invalid -opcode, an out-of-range or misaligned label, a jump to a non-leader, too many -backtrack labels, a four-character load in a wide program, a case-insensitive -backreference on a two-byte subject with no comparison helper available). -*Run-time* bails return retry: backtrack stack overflow, a backtrack budget of -2^27 exceeded, the explicit break opcode, an unmatched dispatcher id, or running -off the end. - -Two deliberate divergences from the interpreter: the packed return-code operand -of the pop opcode is ignored (safe, because the engine hard-codes no backtrack -limit), and **there is no interrupt check** — the interpreter polls at every -backtrack pop; here only the budget bounds a runaway match. - -**Calling in.** The engine resolves a matcher **once per compiled regex**, -caching the result on the shared regex object: a linear scan comparing flags and -pattern text against the published descriptor table, with a sticky negative. -Then the table index is cast straight to a function pointer and called under a -no-GC guard, with the flat characters and the match-pairs array passed directly -— so **captures come back with zero marshalling**, written straight into the -engine's own pairs vector. A retry return makes the caller rerun normally. - -The highest-risk surface in the file is the hand-inlined Latin-1 case folding -used by case-insensitive backreferences: it reimplements the engine's table -rather than calling into it, and the file has no tests. - ---- - -## 11. Limitations - -Some of these are deliberate design rulings; some are gaps. They are separated -below, because a reviewer should not have to guess which is which. - -### 11.1 By design - -**No profiling input, ever.** Stated in full in section 1. The compiler works -from bytecode plus snapshot state — program *state*, not an execution trace. No -count-based or first-seen dynamic recording may feed a compile-time decision. -Where static analysis and heuristics reach their limit, the answer is to stop. -The practical cost is real: several places in this document describe a heuristic -that a profile would answer directly. That is accepted. - -**No debugger.** There is no decline for debugger presence anywhere, and the -consequence is that a `debugger;` statement in compiled code **compiles to -nothing** — it is in the no-op group with `Nop` and `JumpTarget`. That is a -silent semantic loss, not a decline. Debug-specific opcodes are declined or -folded into their non-debug equivalents. Observability is preserved only by -the coarse whole-batch decline in the test lane. - -**No frame introspection.** AOT frames are not interpreter frames or JIT frames. -There is no per-frame script/pc/callee descriptor, nothing registers with the -frame iterators, and no frame iterator is referenced anywhere in the runtime. -Compiled frames are invisible to `Error.stack`, the profiler, and the debugger, -by design. - -**No interrupt checks.** `LoopHead` compiles to nothing, so **back edges never -poll**. There is no interrupt helper in the ABI at all, and no interrupt check -anywhere in the tier. An infinite loop in compiled code cannot be interrupted; -there is no slow-script or watchdog path through this tier. What does exist is -an AOT-stack-overflow guard on arms that enter another compiled body directly. - -These three are ratified together: the test suites carry roughly 170 skip -directives keyed on the tier being enabled, dominated by the debugger, -saved-stacks and profiler families. - -**A closed world.** The compiled set is fixed at registration: only what is -syntactically reachable from a registered root (at most 8 roots) is delazified -and compiled. Therefore **`eval`, `new Function`, dynamically loaded scripts and -other realms are never compiled**. Self-hosted builtins are compiled only from a -22-entry allowlist; everything else in the self-hosted realm runs interpreted. - -**No partial compilation.** A script is compiled iff every *reachable* op in it -is translatable. There is no per-pc bail, no mixed frame, and no state in which -half a body is compiled. - -### 11.2 Scripts that stay interpreted - -Every production skip site, exhaustively: - -| reason | granularity | -|---|---| -| bytecode over 128 KiB | per script | -| generator or async body that uses `arguments` | per script | -| an unsupported environment shape | per script | -| an unsupported opcode anywhere reachable | per script | -| the emitted body exceeds 300k SSA values after the full compile-ladder descent | per script | - -The environment gate declines aliased variables under a body scope that is not a -function or global scope — so a closure-using module, eval, non-syntactic or -lexical body scope is refused — and named-lambda environments. - -The unsupported-opcode set is 17 opcodes: - -- **`Resume`** (1) — the `yield*` driver's half of the resume protocol, which - a compiled body would have to run on the *caller* side; delegating - generators therefore decline; -- **`eval`** (4 forms); -- **ES modules** (3): dynamic import, `import.meta`, module imports; -- **explicit resource management / `using`** (3); -- **self-hosting internals** (3); -- **miscellaneous** (3): BigInt literals, non-syntactic global `this`, - environment callee. - -Note what *is* supported, since the list is shorter than one might guess: `with`, -full try/catch/finally with try-note-driven handler routing, for-in and the -iterator protocol, classes, accessors, private fields, and mapped `arguments`. - -One nuance: the driver is a reachability workqueue seeded from pc 0, so an -unsupported opcode in **statically unreachable** bytecode does not decline the -script. The structural pre-gates (environment ops, `arguments`, `new.target`) are -linear scans over all bytecode and therefore do fire on unreachable code. - -### 11.3 Quality cliffs that degrade silently - -These compile, but worse: - -- the context fixpoint exceeding its round cap **discards every context fact** - in the body (with a loud warning; convergence is guaranteed, so a firing is a - bug to chase); -- the compile ladder's middle and bottom rungs produce progressively less - specialised code; -- the inline splice allowlist is much narrower than the compiler's op coverage, - so many callees decline; -- whole-module BigInt-freedom — which enables tighter numeric masks everywhere - — is lost if *any* script in the bundle mentions `BigInt`, a BigInt typed - array, or `eval` (runtime-compiled source does not cost the claim; it is - handled by the inline fuse test instead, and only degrades a run in which it - actually happens); -- an irreducible residual edge keeps the body compiled but **disables LICM** for - it. - -### 11.4 Constraints the tier imposes on the engine - -- **A compiled frame may hold raw GC addresses only in AOT-traced stack - slots**; every other cache of a raw cell address (property ICs, - megamorphic rows, resolved gname rows, and the rest) is purged around a - compacting GC's slices (`NightPurgeMovableCaches`), and the snapshot's own - recorded addresses are re-derived from rooted copies once the last - shrinking GC before sealing has run. -- **32-bit address space only.** Every helper takes a `uint32_t` linear-memory - offset identity-mapped to a host pointer, and the stamp word exists only - because the 32-bit object header has padding to spare. -- **Global value fuses are dead** whenever any script stayed interpreted, and - always in the test lane. -- A compiled non-syntactic-scope script would crash rather than degrade; the - entry point refuses such scripts, and three helpers assert on their absence. ---- - -## 12. Rough edges - -This is an engineering document, so this section exists. Everything here was -found by reading the current source. Items are grouped by what a reviewer should -do about them. - -### 12.1 Correctness risks - -**A suspected off-by-one in the early-key mask.** The stamp's early class key is -12 bits on the Rust side (bits 18..29), but the C++ add check extracts 13 bits -(18..30), which overlaps the RANGES bit. On a constructing object that still -carries RANGES — which is every object the allocation site seeds — the extracted -key reads high, fails the layout bound check, and **clears SLOTS spuriously**. -Exposure is narrowed by call ordering (two of the three call sites run after a -store that has already dropped RANGES), but the property-add path runs before -any store. The direction of the error is safe — it over-clears, never -over-claims — so this is a coverage and performance bug rather than a soundness -bug. It looks like a leftover from the pre-RANGES 13-bit key design. Found by -reading; not reproduced. - -**The regex compiler's Latin-1 case folding is hand-inlined.** Case-insensitive -backreferences on a Latin-1 subject reimplement the engine's folding table -rather than calling into it, and `regex.rs` has no tests. The opcode length -table is likewise hand-copied from the engine header with no cross-check. - -**Prototype mutation does not clear the stamp.** This is sound only because -every stamp claim is about *own* fixed slots. It is an invariant with no check -behind it, and it deserves either a comment at the mutation sites or a hook. - -**A residual irreducibility of about 1%** of scripts in one large benchmark: a -retreating edge to a non-dominating target, root cause unknown. Such bodies are -still compiled (the backend handles them) but **LICM is disabled** for them. -This is tracked in the source and is an open item. - -### 12.2 Stale source comments that will mislead a reader - -The code moved faster than its own prose in several places. Anyone reading these -files should know: - -- **`translate.rs`'s module header describes the deleted per-op translator.** - The file is now a substrate of shared types, constants and scans, plus a shim - into `bbv.rs`; from a certain point on it is tests. -- `lib.rs` and `view.rs` refer to an intraprocedural type prepass. That module - was removed; the codegen derives its proofs from the interval algebra. -- The `README` refers to `wasm/intra.rs`, `wasm/relooper.rs`, a vendored - `waffle/` directory, a `tests/` corpus and two smoke scripts. None of those - paths exist any more; waffle is a normal crates.io dependency, and structured - control flow is reconstructed by waffle's own passes. -- `bytecode.rs` claims mapped-`arguments` scripts stay interpreted. They do not; - the flag only blocks inlining and some optimisations. -- The ABI header says the pre-write barrier is unnecessary because incremental - GC is disabled. The emitter emits the gate anyway. One of the two is wrong and - it is worth resolving which. -- One store-side discipline is documented as defaulting off; it defaults on. - -### 12.3 Dead or unconsumed machinery - -Not harmful, but it is weight, and a reader will otherwise waste time on it. - -- The runtime layout validator survives only as a diagnostic hook; the - production validator is emitted inline. -- **The prediction's transfer function has not been extracted from the - emitter.** The prediction is now a declared pass with a declared output - (`bbv/predict.rs`), it runs one fixpoint, and `Code` is a consult-only - consumer of it — but the transfer is still *computed by* running `emit_op` - with the IR primitives suppressed (`EmitMode::ContextOnly`). Writing an - independent abstract semantics for the opcode set, family by family, is the - remaining work. Until it exists, `EmitMode` names two ways to run one body - of code rather than two bodies that can disagree; and `strip_all` plus the - closure check stay as the release-mode net for exactly that. -- `iv_grow` is fixpoint metadata riding in the prediction. It carries no claim - and should not survive the transfer function's extraction. - -### 12.4 Known imprecision, deliberate or otherwise - -- **Numeric write-back from guards is inert.** The provenance mechanism that - makes a proven fact durable in a lineage writes back **class facts only**; - every numeric write-back call returns immediately. This was measured - (re-typing a context slot hands out a different carrier representation and - pays a conversion on every edge), but the surrounding comments read as though - a passed tag guard makes the next arithmetic op on that slot checkless. It - does not. -- **`ToBoolean` has no `F64` fast case**, so an unboxed double at a branch pays - a canonicalising box plus the entire five-way tag ladder including a - statically-true branch. It also never consults the operand's interval. -- **The `length` arm never types its int32 result**, even though the string, - array and arguments paths all just produced a proven int32, so every consumer - of `.length` re-tests the tag. There is no comment defending this. -- **Emitting the set cache kills every live SLOTS fact in the body**, including - when the add-transition arm is not emitted and no add can occur — more - conservative than the stated justification requires. -- **Dense element stores clear the object's TYPES and RANGES bits** through the - shared engine store check, even though those bits describe named slot fields. - Sound, but array-heavy code drops the header claim on every non-number element - write. -- The bit-operation slow arm is the only arithmetic slow arm that does not carry - the result interval forward, which drops the slot's fact at the successor join - for every lineage. No comment justifies the asymmetry. -- The analysis's callable-set cap silently drops identities past 8; the record of - what was dropped is kept and never consumed. -- The prototype chain walk stops at depth 8 without recording that it truncated. - -### 12.5 Where the complexity genuinely lives - -Three mechanisms are intricate enough that a change to them needs real care, and -the document has tried to explain rather than summarise them: - -- **`theta` and the token discipline** (sections 4.6-4.9). The reducibility - argument is a chain of four structural properties, and the residual 1% shows - the chain is not yet airtight. -- **The inline splice frame layout** (section 5.7). A child frame laid down - inside the caller's operand region, with a hand-inlined prologue, a fact - transfer in both directions, and an exception rule that depends on two - admission refusals holding simultaneously. -- **The stamp bits** (section 2.2). Three independent validity claims on one - word, maintained by two engine hooks and one compiled twin, with a - construction sentinel sharing the same bits. - -Of these, the stamp is the one whose invariant is most load-bearing and least -locally checkable, which is why it is documented first in this document rather -than last. - ---- - -## 13. Glossary - -| term | meaning | -|---|---| -| **arm** | one branch of a lowering's dispatch. Ends either in a continuation to the successor pc under its own facts, or in a merge inside the same version | -| **BBV** | basic-block versioning: compiling one code version per (pc, abstract state). Here the two halves are separated: a *block* is `Ver { pc, class, track, depth }`, and the abstract state is a *prediction* keyed by pc alone | -| **carrier** | an unboxed SSA value that crosses version boundaries as a block parameter, instead of being reloaded from the frame. Not the same as `Ctx::carried`, which is *which* locals ride the edge, in whatever representation | -| **prediction** | the fact context at a program point: what the analysis says holds there, which codegen enforces (section 4.2). One per pc on Opt; GEN has none | -| **class key** | the analysis's numbering of a discovered class; `key + 1` is what a stamp's low half holds | -| **clean miss** | a cache miss the runtime served without running user code, allocating or reshaping, so the caller rejoins its happy-path lineage | -| **context (`Ctx`)** | the abstract state a version is compiled under: per-slot facts, tokens, carrier sets and track | -| **continuation** | an edge from a lowering to a successor pc, routed through the merge point | -| **effect word** | the two-bit provenance value a compiled body returns describing what it disturbed | -| **fenced hierarchy** | the four-width ladder of property facts, all discharged by one guard form | -| **fuse** | a memory word that *is* a soundness guard: baked-constant reads test it, and any write through the engine blows it | -| **likely fact** | anything the analysis produces; always a prediction, never trusted | -| **on-ramp** | a guard chain letting a degraded lineage re-enter a loop header's optimised version by re-proving its context | -| **quiet alloc** | a helper that may GC but writes no pre-existing user-visible heap, so it sweeps raw pointers only | -| **segment** | a spliced callee's synthetic pc range above the root script's bytecode | -| **side entry** | an edge into a loop's interior that bypasses its header | -| **splice** | inlining, done by mapping the callee's bytecode into the caller's pc space rather than by importing a CFG | -| **stamp** | the 32-bit class-and-validity word in the object header (section 2.2) | -| **token** | a per-enclosing-loop layer marker in a context; part of version identity, never a fact | -| **track** | `Opt`, `Side` or `Dirty`: how control reached this version. Part of version identity; only ever descends | -| **version** | one compiled copy of one pc, identified by `(pc, token class, track, depth)` | +| identity | object has one compatible layout key | select a property layout | wholesale clear or restamp | +| TYPES/SHALLOW | guarded fields retain their numeric type property | remove redundant numberness checks | a non-number protected-field store | +| SLOTS | properties occupy predicted slots | bake a fixed-slot offset | an unexpected in-prefix property addition | +| RANGES | protected fields retain predicted intervals | propagate interval proofs | every unchecked engine store or an out-of-range compiled store | + +Correctness requires an invalidation cut across all mutation paths: engine +stores call the NightMonkey store check; compiled stores perform equivalent +maintenance inline; property additions call `NightAddPropCheck`; shape and +prototype mutations clear identity when required; and GC forwarding either +updates roots or invalidates raw-pointer caches. + +The construction sentinel prevents partial objects passing identity guards. +Constructor exit publishes identity only after checked stores establish the +surviving validity bits. Compatible prefix layouts receive contiguous keys so +one range check can cover a layout *region*. Older comments call a region a +*clump*. + +## 7. Caches and fuses + +A fuse is a rarely changing condition represented by an armed or blown word. +It is safe only if every operation that can falsify it reaches an invalidation +point. NightMonkey uses fuses or generations for bindings, call and constructor +targets, builtins, prototype-dependent operations, and dynamically compiled +source. + +Global property hooks invalidate binding fuses for interpreted writes. Property +cache rows publish their validity word last so generated code cannot see a +partial row. Major GC clears raw shape/prototype caches; minor GC clears or +rebuilds subsets that can hold nursery pointers. + +The dynamic-source fuse is intended to be monotone after capture. Re-arming is +sound only when registration accounts for every source compiled so far and no +unregistered compilation is interleaved. + +## 8. Runtime ABI and memory regions + +`NightEnv.h` defines the ordered environment descriptor shared by compiler and +runtime; `compiler/build.rs` generates its Rust mirror. `NightRegionShape.h` +similarly shares cache sizes and row strides. These are the preferred pattern +for cross-language ABI data. + +The environment contains atom, layout, binding, fuse, builtin, regex, and cache +tables plus mutable cells. Snapshot region words are absolute addresses. +In-process table words are descriptor-relative offsets and are rebased during +installation; address and length words remain absolute. + +`NightHelperList.h` is the C++ helper manifest. Rust's `Helpers` structure and +resolution logic manually duplicate it and should be generated from the same +source. `bbv/abi.rs` records SpiderMonkey offsets and selector values baked into +Wasm. `NightInlineHeap.cpp` statically asserts the engine-layout half. New ABI +data should use generated shared definitions rather than paired literals. + +## 9. Entry, stack, and GC + +`NightStack` is a separately allocated array of boxed values owned by +`JSContext`; its live prefix is traced as roots. `AutoNightReentry` restores the +old top after interpreter-to-AOT reentry. + +An entry frame contains callee, receiver, actual/formal slots, locals, and +operand storage. Missing actuals are padded with `undefined`. The complete frame +must fit before entry. Bounds checks must compare integer slot counts before +forming an end pointer, because an already-out-of-range C++ pointer is undefined +even if it is used only in a comparison. + +Generated code may retain non-GC scalars in SSA across calls. GC pointers that +survive a may-GC operation must be in the live stack prefix or another root. +Inline heap writes mirror SpiderMonkey barriers using offsets pinned by static +assertions. + +Most installed state, caches, persistent roots, and callbacks are process-global +and have no teardown. This matches the single-image shell deployment but must +be enforced or redesigned before runtime destruction and recreation. + +## 10. Generators, async, exceptions, and regexps + +Generator and async bodies have generic-track support in `bbv/generator.rs` and +`NightGenerator.cpp`. Suspension stores locals and operands in an engine object +using an AOT-owned layout; resume re-enters the same compiled script. An +AOT-suspended generator cannot resume in the interpreter. Bodies needing an +arguments object remain unsupported, and resumable bodies do not receive the +ordinary optimistic specialization. + +Try notes feed CFG and frame construction. Calls and helpers propagate pending +exceptions through the error result. Inlining rejects contexts whose exception +or frame semantics cannot be represented by a splice. + +`wasm/regex.rs` translates supported irregexp bytecode to Wasm. Runtime matching +selects installed matchers by pattern and flags. A matcher returns success, +failure, or retry; retry and unsupported programs use irregexp's established +fallback. Exhausting fixed backtracking storage must return retry. + +## 11. Limits and policy + +`constants.rs` is intended to collect analysis, specialization, inlining, +translation, and diagnostic policy. Structural ABI limits stay with their +representations. Some limits are scattered throughout the implementations of +various heuristics; cleanup is ongoing. + +## 12. Capabilities and fallback + +The exhaustive match in `bbv/ops.rs` determines lowering support. Explicit +declines include BigInt literals, eval variants, module imports, +explicit-resource-management operations, and interpreter/debugger escapes. +Environment gates reject frame shapes the runtime cannot model. + +Opcode knowledge is also duplicated in analysis transfer, effects, splice +safety, visualization, and auxiliary scans. An exhaustive lowering match does +not make those classifications exhaustive. A single opcode capability +description should state stack and immediate shape, analysis transfer, effects, +frame/script relativity, splice safety, and lowering support. New opcodes should +fail tests until every dimension is classified. + +Fallback has three levels: + +1. failed specialization continues in generic compiled code; +2. helpers and regex matchers use established engine slow paths; +3. untranslatable scripts have no AOT entry and remain interpreted. + +Tests must distinguish these. Passing a language test through a shell that can +silently interpret a declined script does not establish generated-code coverage. + +## 13. Validation + +Validation includes Rust unit tests, jit-tests and jstests in AOT and +interpreter-only wasm lanes, differential application runs, Wasm validation, +post-translation reducibility checks, C++ assertions for baked layouts, and +diagnostics for degradation, skipped scripts, guards, effects, and caches. + +## 14. Glossary + +- **likely fact**: an untrusted analysis prediction. +- **codegen fact**: a property proved by a guard or sound producer and carried + in a BBV context. +- **track**: optimistic or generic execution state in version identity. +- **side arm**: a guarded alternative inside one lowering. +- **rung**: a retry step of the compile ladder with less specialization. +- **theta**: the continuation and version-interning logic between operations. +- **carrier**: an unboxed SSA value passed between versions. +- **splice**: an inlined callee represented as synthetic bytecode. +- **stamp**: the object word identifying layout and valid subclaims. +- **region**: a compatible contiguous family of layout keys. +- **fuse**: an armed condition with a complete invalidation cut. +- **choke/chokepoint**: an invalidation point. Prefer that standard term in new + code. +- **dirty**: a lineage whose facts were weakened by effects or a failed route. +- **on-ramp**: a guarded edge from a weak lineage to an optimistic context. From a9a3a18fb5c806c9d7faf92a9e8b564ed50b66a0 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Sat, 5 Sep 2026 14:54:21 -0700 Subject: [PATCH 5/7] Add missing mozconfig for CI. --- .github/workflows/mozconfig-nightmonkey | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/workflows/mozconfig-nightmonkey diff --git a/.github/workflows/mozconfig-nightmonkey b/.github/workflows/mozconfig-nightmonkey new file mode 100644 index 0000000000000..8e20997a05b7b --- /dev/null +++ b/.github/workflows/mozconfig-nightmonkey @@ -0,0 +1,4 @@ +# The NightMonkey in-process test shell (wasm32-wasi) plus the wasm-jit-runner +# host that runs it. The sourced config sets MOZ_OBJDIR=obj-nightmonkey-inprocess, +# where the test step finds dist/bin/inproc-shell.sh to use as the shell. +. "$topsrcdir/js/src/night/configs/mozconfig-nightmonkey-inprocess" From 1a59af7577c68c5f1f1fb7e2e3368fa92c732e79 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Sat, 5 Sep 2026 19:08:12 -0700 Subject: [PATCH 6/7] Try to fix CI by adding the WASI sysroot as well. --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ecb45431733d..ab14b80ecea7f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,10 @@ jobs: submodules: true - run: ./mach --no-interactive bootstrap --application-choice=js - if: matrix.mozconfig == 'nightmonkey' - run: rustup target add wasm32-wasip1 + # mach bootstrap only fetches host-target toolchains; the wasm32-wasi + # sysroot (crt1.o, libc) must be fetched explicitly into ~/.mozbuild. + run: | + rustup target add wasm32-wasip1 + cd ~/.mozbuild && "$GITHUB_WORKSPACE/mach" --log-no-times artifact toolchain --from-build sysroot-wasm32-wasi - run: MOZCONFIG=.github/workflows/mozconfig-${{ matrix.mozconfig }} ./mach build - run: MOZCONFIG=.github/workflows/mozconfig-${{ matrix.mozconfig }} ./mach ${{ matrix.test }} ${{ matrix.mozconfig == 'nightmonkey' && '--shell obj-nightmonkey-inprocess/dist/bin/inproc-shell.sh' || '' }} ${{ matrix.test == 'jit-test' && '--exclude wasm/atomicity.js' || '' }} From 0d1656f94ed7446f62c6f1184c4a2601586a9f18 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Sat, 5 Sep 2026 21:06:44 -0700 Subject: [PATCH 7/7] fix weird rustup toolchain install race where rust-src is not installed --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab14b80ecea7f..65aa44c883dba 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,7 @@ jobs: # mach bootstrap only fetches host-target toolchains; the wasm32-wasi # sysroot (crt1.o, libc) must be fetched explicitly into ~/.mozbuild. run: | + rustup component add rust-src rustup target add wasm32-wasip1 cd ~/.mozbuild && "$GITHUB_WORKSPACE/mach" --log-no-times artifact toolchain --from-build sysroot-wasm32-wasi - run: MOZCONFIG=.github/workflows/mozconfig-${{ matrix.mozconfig }} ./mach build