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/.github/workflows/main.yml b/.github/workflows/main.yml index 0840e00500ec4..65aa44c883dba 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,18 +8,25 @@ 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' + # 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 - - 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/.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" 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/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) 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..792645253ece2 --- /dev/null +++ b/js/src/night/README.md @@ -0,0 +1,262 @@ +# 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`/`jstests.py` shim running the wasm shell under the runner; the in-process build installs a copy into `dist/bin`. | + +## 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 +``` + +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: + +- `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..e877343a11b4c --- /dev/null +++ b/js/src/night/build_wasm_jit_runner.py @@ -0,0 +1,57 @@ +# 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) + + # 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/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..8c572740b07c4 100644 --- a/js/src/tests/jstests.list +++ b/js/src/tests/jstests.list @@ -43,6 +43,180 @@ 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")) 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 +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. +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);