diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0310208 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: CI +concurrency: + group: ${{ github.head_ref || github.ref }}-${{ github.workflow }} + cancel-in-progress: true +on: + push: + branches: [main] + pull_request: + workflow_dispatch: +defaults: + run: + shell: bash + +# NightMonkey builds against a pinned branch of the SpiderMonkey fork that +# carries the external compiler hook API. +env: + FIREFOX_REPO: bytecodealliance/firefox + FIREFOX_REF: wasi-ff147 + SM_OBJDIR: obj-nightmonkey-sm + +jobs: + jit-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Resolve the SpiderMonkey revision + id: smrev + run: | + sha=$(git ls-remote "https://github.com/$FIREFOX_REPO" "refs/heads/$FIREFOX_REF" | cut -f1) + if [ -z "$sha" ]; then + echo "::error::branch $FIREFOX_REF not found on https://github.com/$FIREFOX_REPO" + exit 1 + fi + echo "sha=$sha" >> "$GITHUB_OUTPUT" + + - name: Cache the SpiderMonkey checkout + id: cache-src + uses: actions/cache@v4 + with: + path: firefox + key: firefox-src-${{ steps.smrev.outputs.sha }} + + - name: Fetch SpiderMonkey (shallow) + if: steps.cache-src.outputs.cache-hit != 'true' + run: | + git init firefox + cd firefox + git remote add origin "https://github.com/$FIREFOX_REPO" + git fetch --depth 1 origin "${{ steps.smrev.outputs.sha }}" + git checkout --detach FETCH_HEAD + + - name: Cache the mach toolchains + uses: actions/cache@v4 + with: + path: ~/.mozbuild + key: mozbuild-${{ runner.os }}-${{ steps.smrev.outputs.sha }} + restore-keys: mozbuild-${{ runner.os }}- + + - name: Bootstrap SpiderMonkey + run: | + cd firefox + ./mach --no-interactive bootstrap --application-choice=js + # mach bootstrap only fetches host-target toolchains; the wasm32-wasi + # sysroot (crt1.o, libc) must be fetched explicitly into ~/.mozbuild. + rustup component add rust-src + rustup target add wasm32-wasip1 + cd ~/.mozbuild && "$GITHUB_WORKSPACE/firefox/mach" --log-no-times artifact toolchain --from-build sysroot-wasm32-wasi + + - name: Cache the SpiderMonkey objdir + uses: actions/cache@v4 + with: + path: firefox/${{ env.SM_OBJDIR }} + key: firefox-obj-${{ runner.os }}-${{ steps.smrev.outputs.sha }}-${{ hashFiles('spidermonkey/mozconfig') }} + + - name: Build SpiderMonkey + run: scripts/build-spidermonkey.sh firefox + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + build/cargo + key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} + restore-keys: cargo-${{ runner.os }}- + + - name: Build NightMonkey + run: | + cmake -S . -B build -DSPIDERMONKEY_DIST="$GITHUB_WORKSPACE/firefox/$SM_OBJDIR/dist" + cmake --build build + + - name: jit-tests (AOT lane) + run: scripts/run-jit-tests.sh firefox build -- --format=automation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fcd1e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build*/ +target/ +__pycache__/ +*.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..69086db --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,255 @@ +# NightMonkey: an AOT compiler from JavaScript to Wasm, layered on top of +# SpiderMonkey. This build consumes the dist/ directory of a SpiderMonkey +# build made with --enable-external-compiler-hooks (spidermonkey/mozconfig): +# its private headers, the compile flags libjs was built with +# (include-private/js-build-config.json) and the archives under lib/. +# +# cmake -S . -B build -DSPIDERMONKEY_DIST=/path/to/firefox/obj-nightmonkey-sm/dist +# cmake --build build +# +# Produces, under build/bin: +# js the NightMonkey wasm shell (the snapshot flow's shell) +# js-inproc the same shell with the in-process compiler linked in +# (the test lane; NIGHT_INPROCESS) +# nightmonkey the host AOT compiler (snapshot in, module out) +# wasm-jit-runner the host that runs the in-process test lane +# inproc-shell.sh the shell wrapper the test harnesses use + +cmake_minimum_required(VERSION 3.20) + +set(SPIDERMONKEY_DIST "" CACHE PATH + "dist/ directory of a SpiderMonkey build with --enable-external-compiler-hooks") +option(NIGHT_INPROCESS "Build the in-process test lane (guest compiler, wasm-jit-runner)" ON) +option(NIGHT_DEBUG "Runtime diagnostics and crash-on-failure in the in-process lane" OFF) +set(NIGHT_CARGO "cargo" CACHE STRING "cargo executable") +set(NIGHT_STACK_SIZE "67108864" CACHE STRING "linear-memory stack size of the shells, in bytes") +set(NIGHT_CARGO_TARGET_DIR "" CACHE PATH "cargo target directory (default: /cargo; share it between build trees)") +set(NIGHT_ENGINE_VERSION "ff147" CACHE STRING + "engine version the compiler is built for: a checked-in opcode table compiler/src/opcodes/.rs and the cargo feature of the same name") + +if(NOT SPIDERMONKEY_DIST) + message(FATAL_ERROR "SPIDERMONKEY_DIST is required: the dist/ directory of a SpiderMonkey build made with --enable-external-compiler-hooks (see spidermonkey/mozconfig)") +endif() +get_filename_component(SPIDERMONKEY_DIST "${SPIDERMONKEY_DIST}" ABSOLUTE) +set(SM_CONFIG_JSON "${SPIDERMONKEY_DIST}/include-private/js-build-config.json") +if(NOT EXISTS "${SM_CONFIG_JSON}") + message(FATAL_ERROR "${SM_CONFIG_JSON} not found: is this a --enable-external-compiler-hooks build?") +endif() +file(READ "${SM_CONFIG_JSON}" SM_JSON) + +function(sm_json_list out key) + string(JSON n LENGTH "${SM_JSON}" ${key}) + set(result) + if(n GREATER 0) + math(EXPR last "${n} - 1") + foreach(i RANGE ${last}) + string(JSON v GET "${SM_JSON}" ${key} ${i}) + list(APPEND result "${v}") + endforeach() + endif() + set(${out} "${result}" PARENT_SCOPE) +endfunction() + +# The compiler command lines libjs was built with: the executable plus the +# sysroot, standard and target flags. +string(JSON SM_CXX_CMD GET "${SM_JSON}" cxx) +string(JSON SM_CC_CMD GET "${SM_JSON}" cc) +separate_arguments(SM_CXX_CMD NATIVE_COMMAND "${SM_CXX_CMD}") +separate_arguments(SM_CC_CMD NATIVE_COMMAND "${SM_CC_CMD}") +list(GET SM_CXX_CMD 0 SM_CXX) +list(GET SM_CC_CMD 0 SM_CC) +list(REMOVE_AT SM_CXX_CMD 0) +list(REMOVE_AT SM_CC_CMD 0) +sm_json_list(SM_OS_CXXFLAGS os_cxxflags) +sm_json_list(SM_OPTIMIZE_FLAGS optimize_flags) +sm_json_list(SM_DEBUG_FLAGS debug_flags) +sm_json_list(SM_EXTRA_CXXFLAGS extra_cxxflags) +sm_json_list(SM_DEBUG_DEFINES debug_defines) +sm_json_list(SM_LIBRARY_DEFINES library_defines) +sm_json_list(SM_FORCE_INCLUDES force_includes) +sm_json_list(SM_INCLUDE_DIRS include_dirs) +sm_json_list(SM_LIBRARIES libraries) +string(JSON SM_RUST_TARGET GET "${SM_JSON}" rust_target) +string(JSON SM_OS_ARCH GET "${SM_JSON}" os_arch) + +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR wasm32) +set(CMAKE_C_COMPILER "${SM_CC}") +set(CMAKE_CXX_COMPILER "${SM_CXX}") +set(CMAKE_C_COMPILER_WORKS TRUE) +set(CMAKE_CXX_COMPILER_WORKS TRUE) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(CMAKE_EXECUTABLE_SUFFIX "") + +project(nightmonkey C CXX) + +# Every engine-facing TU is compiled exactly as libjs was: same sysroot, +# standard, target, ABI-affecting flags, defines and force-included +# configuration headers, so the private headers describe the same layouts. +set(NIGHT_CXX_FLAGS ${SM_CXX_CMD} ${SM_OS_CXXFLAGS} ${SM_OPTIMIZE_FLAGS} + ${SM_DEBUG_FLAGS} ${SM_EXTRA_CXXFLAGS}) +set(NIGHT_DEFINES ENABLE_JS_NIGHTMONKEY=1) +foreach(d ${SM_DEBUG_DEFINES}) + list(APPEND NIGHT_DEFINES "${d}=1") +endforeach() +foreach(d ${SM_LIBRARY_DEFINES}) + list(APPEND NIGHT_DEFINES "${d}") +endforeach() +if(NIGHT_DEBUG) + list(APPEND NIGHT_DEFINES NIGHTMONKEY_DEBUG=1) +endif() +set(NIGHT_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}") +foreach(d ${SM_INCLUDE_DIRS}) + list(APPEND NIGHT_INCLUDE_DIRS "${SPIDERMONKEY_DIST}/${d}") +endforeach() +set(NIGHT_FORCE_INCLUDE_FLAGS) +foreach(h ${SM_FORCE_INCLUDES}) + # SHELL: keeps CMake from de-duplicating the repeated -include. + list(APPEND NIGHT_FORCE_INCLUDE_FLAGS "SHELL:-include ${SPIDERMONKEY_DIST}/include-private/${h}") +endforeach() + +function(night_engine_target name) + target_compile_options(${name} PRIVATE ${NIGHT_CXX_FLAGS} ${NIGHT_FORCE_INCLUDE_FLAGS} + -Wall -Wno-invalid-offsetof -Wno-unused-private-field) + target_compile_definitions(${name} PRIVATE ${NIGHT_DEFINES}) + target_include_directories(${name} PRIVATE ${NIGHT_INCLUDE_DIRS}) +endfunction() + +# The in-process sources are compiled into the runtime archive but only pulled +# into a shell that references them (js-inproc); the snapshot shell stays free +# of the runner's hostcall imports. +function(night_inprocess_target name) + target_compile_definitions(${name} PRIVATE ENABLE_JS_NIGHTMONKEY_INPROCESS=1) +endfunction() + +# --- the night runtime ------------------------------------------------------- +set(NIGHT_RUNTIME_SOURCES + runtime/NightEntry.cpp + runtime/NightGenerator.cpp + runtime/NightHooks.cpp + runtime/NightInlineCaches.cpp + runtime/NightInlineHeap.cpp + runtime/NightOps.cpp + runtime/NightOpsInterp.cpp + runtime/NightRegExp.cpp + runtime/NightRegistration.cpp + runtime/NightRuntime.cpp + runtime/NightRuntimeSlots.cpp + runtime/NightSnapshotExtras.cpp + runtime/NightStack.cpp) +if(NIGHT_INPROCESS) + list(APPEND NIGHT_RUNTIME_SOURCES runtime/NightInproc.cpp runtime/NightInprocHost.cpp) +endif() +add_library(night_runtime STATIC ${NIGHT_RUNTIME_SOURCES}) +night_engine_target(night_runtime) +if(NIGHT_INPROCESS) + night_inprocess_target(night_runtime) +endif() + +# --- the opcode table check ------------------------------------------------------ +# The compiler's opcode table is generated ahead of time from the engine's +# Opcodes.h and checked in (compiler/src/opcodes/.rs, selected by +# the cargo feature of the same name). This checks that the table for +# NIGHT_ENGINE_VERSION is what the SpiderMonkey being built against +# generates, so an engine with different bytecode is refused rather than +# miscompiled. Everything built against the engine depends on it. +find_program(NIGHT_PYTHON python3 REQUIRED DOC "python3, for scripts/gen_opcodes.py") +add_custom_target(night_opcodes_check + COMMAND ${NIGHT_PYTHON} "${CMAKE_SOURCE_DIR}/scripts/gen_opcodes.py" check + ${NIGHT_ENGINE_VERSION} "${SPIDERMONKEY_DIST}/include-private/vm/Opcodes.h" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Checking the ${NIGHT_ENGINE_VERSION} opcode table against the engine's Opcodes.h" + VERBATIM) +add_dependencies(night_runtime night_opcodes_check) + +# --- cargo-built pieces ---------------------------------------------------------- +set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/cargo") +if(NIGHT_CARGO_TARGET_DIR) + set(CARGO_TARGET_DIR "${NIGHT_CARGO_TARGET_DIR}") +endif() +set(CARGO_ENV "CARGO_TARGET_DIR=${CARGO_TARGET_DIR}") +# The engine version, as a cargo feature. +set(CARGO_ENGINE_FEATURES --no-default-features --features ${NIGHT_ENGINE_VERSION}) +set(NIGHT_BIN_DIR "${CMAKE_BINARY_DIR}/bin") +file(MAKE_DIRECTORY "${NIGHT_BIN_DIR}") + +# The host AOT compiler (snapshot in, module out) with wizer driving built in. +add_custom_target(nightmonkey_host ALL + COMMAND ${CMAKE_COMMAND} -E env ${CARGO_ENV} RUSTFLAGS=-Cdebuginfo=0 + ${NIGHT_CARGO} build --release -p nightmonkey ${CARGO_ENGINE_FEATURES} --features wizen + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CARGO_TARGET_DIR}/release/nightmonkey" "${NIGHT_BIN_DIR}/nightmonkey" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + BYPRODUCTS "${NIGHT_BIN_DIR}/nightmonkey" + COMMENT "cargo: nightmonkey (host)" + VERBATIM) +add_dependencies(nightmonkey_host night_opcodes_check) + +if(NIGHT_INPROCESS) + # The guest compiler: night-compiler and night-snapshot as one staticlib + # for the shell's target, linked into the shell next to libjsrust.a. + set(NIGHT_GUEST_LIB "${CARGO_TARGET_DIR}/${SM_RUST_TARGET}/release/libnight_guest.a") + add_custom_target(night_guest ALL + COMMAND ${CMAKE_COMMAND} -E env ${CARGO_ENV} RUSTFLAGS=-Cdebuginfo=0 + ${NIGHT_CARGO} build --release -p night-guest ${CARGO_ENGINE_FEATURES} --target ${SM_RUST_TARGET} + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + BYPRODUCTS "${NIGHT_GUEST_LIB}" + COMMENT "cargo: night-guest (${SM_RUST_TARGET})" + VERBATIM) + add_dependencies(night_guest night_opcodes_check) + + # The host that runs the in-process lane (hostcalls that inject compiled + # bodies into the running shell instance). + add_custom_target(wasm_jit_runner ALL + COMMAND ${CMAKE_COMMAND} -E env ${CARGO_ENV} RUSTFLAGS=-Cdebuginfo=0 + ${NIGHT_CARGO} build --release -p wasm-jit-runner + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CARGO_TARGET_DIR}/release/wasm-jit-runner" "${NIGHT_BIN_DIR}/wasm-jit-runner" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + BYPRODUCTS "${NIGHT_BIN_DIR}/wasm-jit-runner" + COMMENT "cargo: wasm-jit-runner (host)" + VERBATIM) +endif() + +# --- the shells -------------------------------------------------------------------- +set(NIGHT_SM_LIBS) +foreach(l ${SM_LIBRARIES}) + list(APPEND NIGHT_SM_LIBS "${SPIDERMONKEY_DIST}/${l}") +endforeach() + +# Links a NightMonkey shell: the runtime, SpiderMonkey's shell/engine +# archives and the libs. The memory and funcref table are exported (and the +# table growable) so appended and injected code can share them; the deep +# stack is for the in-process compiler's lowering, which recurses on the main +# stack. The linker driver needs the same sysroot/target as the compiler. +function(night_shell name) + add_executable(${name} shell/nightshell.cpp) + night_engine_target(${name}) + target_include_directories(${name} PRIVATE "${CMAKE_SOURCE_DIR}/third_party/wizer") + set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${NIGHT_BIN_DIR}") + target_link_libraries(${name} PRIVATE night_runtime ${NIGHT_SM_LIBS} ${ARGN} m) + target_link_options(${name} PRIVATE ${SM_CXX_CMD} + -Wl,-z,stack-size=${NIGHT_STACK_SIZE} -Wl,--stack-first + -Wl,--export-memory -Wl,--export-table -Wl,--growable-table + -lwasi-emulated-process-clocks -lwasi-emulated-getpid) +endfunction() + +# The snapshot flow's shell: what `nightmonkey --shell` wizens. +night_shell(js) + +if(NIGHT_INPROCESS) + # The test lane's shell, with the guest compiler linked in. Two Rust static + # libraries meet here: SpiderMonkey's libjsrust.a, whose LTO-merged object + # carries its own copy of std's unmangled runtime symbols + # (rust_eh_personality, the panic and allocator shims), and the guest + # compiler with the prebuilt std. The first definition wins and every copy + # routes through the same allocator shim. + night_shell(js-inproc "${NIGHT_GUEST_LIB}") + night_inprocess_target(js-inproc) + add_dependencies(js-inproc night_guest) + target_link_options(js-inproc PRIVATE -Wl,--allow-multiple-definition) + + # The harness wrapper: runs js-inproc under wasm-jit-runner. + configure_file(scripts/inproc-shell.sh.in "${NIGHT_BIN_DIR}/inproc-shell.sh" @ONLY) + file(CHMOD "${NIGHT_BIN_DIR}/inproc-shell.sh" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +endif() diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..14db694 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,49 @@ +# Contributor Covenant Code of Conduct + +*Note*: this Code of Conduct pertains to individuals' behavior. Please also see the [Organizational Code of Conduct][OCoC]. + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Bytecode Alliance CoC team at [report@bytecodealliance.org](mailto:report@bytecodealliance.org). The CoC team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The CoC team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the Bytecode Alliance's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[OCoC]: https://github.com/bytecodealliance/wasmtime/blob/main/ORG_CODE_OF_CONDUCT.md +[homepage]: https://www.contributor-covenant.org +[version]: https://www.contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3c7036b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing to weval + +## Code of Conduct + +NightMonkey is a [Bytecode Alliance] project. It follows the Bytecode +Alliance's [Code of Conduct] and [Organizational Code of Conduct]. + +[Bytecode Alliance]: https://bytecodealliance.org/ +[Code of Conduct]: CODE_OF_CONDUCT.md +[Organizational Code of Conduct]: ORG_CODE_OF_CONDUCT.md diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..50596e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3536 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli 0.31.1", +] + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli 0.33.0", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object 0.39.1", +] + +[[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.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[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 = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cap-fs-ext" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476f0d0003a760918ed4b1e039a59e11769030416f79c8222551d22785f7f70d" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150941cefd3df4de2fea24604ba4949371576f62e527410298333f7d431a1bc6" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.5", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0bf07d379916947be6c4a07f43684153d710a2896c31f9e97781362895596c" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.5", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec6a5b75f54547c579a6b117c6fdd5f04f4ab7598de747b9f440a53592b3a4a" +dependencies = [ + "ambient-authority", + "rand 0.8.8", +] + +[[package]] +name = "cap-std" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59e59fa26472d29680ece6a9f8ee8b0551a719a33df2f5240bde065ecbddfd7" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.5", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54c289326c70f1c697ebf0a31842a480932e5942b5fac92fcc46e87286b48e2" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.5", + "winx", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4b56ebe316895d3fa37775d0a87b0c889cc933f5c8b253dbcc7c7bcb7fe7e4" +dependencies = [ + "cranelift-assembler-x64-meta 0.118.0", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d521bdbc6098937af83ef4ab6d5c07398126bc71878f7ef4ea9499977978ef5" +dependencies = [ + "cranelift-assembler-x64-meta 0.132.3", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cabbc01dfbd7dcd6c329ca44f0212910309c221797ac736a67a5bc8857fe1b" + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dde0b83164d4a497860af4236271178bc640512b067101e0853e4f74eaa4df5" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ffe46df300a45f1dc6f609dc808ce963f0e3a2e971682c479a2d13e3b9b8ef" +dependencies = [ + "cranelift-entity 0.118.0", +] + +[[package]] +name = "cranelift-bforest" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0111d110b72b4efad69a372e29e21628652fd0bcab66967e5c8350ab679affd5" +dependencies = [ + "cranelift-entity 0.132.3", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b265bed7c51e1921fdae6419791d31af77d33662ee56d7b0fa0704dc8d231cab" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-bitset" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf01ecc92fc5499789d79c3b817299d5a8ddd828d31dcf0f9cc3cc66f38dbb36" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e606230a7e3a6897d603761baee0d19f88d077f17b996bb5089488a29ae96e41" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64 0.118.0", + "cranelift-bforest 0.118.0", + "cranelift-bitset 0.118.0", + "cranelift-codegen-meta 0.118.0", + "cranelift-codegen-shared 0.118.0", + "cranelift-control 0.118.0", + "cranelift-entity 0.118.0", + "cranelift-isle 0.118.0", + "gimli 0.31.1", + "hashbrown 0.15.5", + "log", + "pulley-interpreter 31.0.0", + "regalloc2 0.11.2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cd2563bead0090c3879a7ff7327f7550c9d59bb5d05ecc8cd7886977aa3125a" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64 0.132.3", + "cranelift-bforest 0.132.3", + "cranelift-bitset 0.132.3", + "cranelift-codegen-meta 0.132.3", + "cranelift-codegen-shared 0.132.3", + "cranelift-control 0.132.3", + "cranelift-entity 0.132.3", + "cranelift-isle 0.132.3", + "gimli 0.33.0", + "hashbrown 0.17.1", + "libm", + "log", + "pulley-interpreter 45.0.3", + "regalloc2 0.15.2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a63bffafc23bc60969ad528e138788495999d935f0adcfd6543cb151ca8637d" +dependencies = [ + "cranelift-assembler-x64 0.118.0", + "cranelift-codegen-shared 0.118.0", + "pulley-interpreter 31.0.0", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9640d250d26f9381a73dc7f9862b27928d4809119aec73be0e556612e67b6a99" +dependencies = [ + "cranelift-assembler-x64-meta 0.132.3", + "cranelift-codegen-shared 0.132.3", + "cranelift-srcgen", + "heck", + "pulley-interpreter 45.0.3", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af50281b67324b58e843170a6a5943cf6d387c06f7eeacc9f5696e4ab7ae7d7e" + +[[package]] +name = "cranelift-codegen-shared" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07f156b90efc94371ddb3536f76e4ab671ad2e093bfa5511e10198cadbb0c47" + +[[package]] +name = "cranelift-control" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c20c1b38d1abfbcebb0032e497e71156c0e3b8dcb3f0a92b9863b7bcaec290c" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-control" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a76fd9dd37dcbc3d2d2e00abe3281a7e88adc035fba3b8114cc69981576ec" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2c67d95507c51b4a1ff3f3555fe4bfec36b9e13c1b684ccc602736f5d5f4a2" +dependencies = [ + "cranelift-bitset 0.118.0", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-entity" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eea22522144ba08c7e7ef94bfc25d474ef016c2771974b8ab1986734ac85965" +dependencies = [ + "cranelift-bitset 0.132.3", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e002691cc69c38b54fc7ec93e5be5b744f627d027031d991cc845d1d512d0ce" +dependencies = [ + "cranelift-codegen 0.118.0", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-frontend" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa2826c80dff1d93b19b3cfbcaf9fae44c78d739a98d146dd5bd89c51e14367" +dependencies = [ + "cranelift-codegen 0.132.3", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93588ed1796cbcb0e2ad160403509e2c5d330d80dd6e0014ac6774c7ebac496" + +[[package]] +name = "cranelift-isle" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e238a69b95c5415456f22189494a12db94f313bb72b6ec9cc88ddf2f1056e28e" + +[[package]] +name = "cranelift-native" +version = "0.118.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b09bdd6407bf5d89661b80cf926ce731c9e8cc184bf49102267a2369a8358e" +dependencies = [ + "cranelift-codegen 0.118.0", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-native" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eceb0ebd8d6aef6bb287e8d532a164b0db1c0062961211e9c22a91f67521c098" +dependencies = [ + "cranelift-codegen 0.132.3", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004643f39a7bec553de5263d650db30e5b9caec1d5cbe065fd732b7ec9ae40d0" + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[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" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[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 = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.5", + "windows-sys 0.59.0", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[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.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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 1.1.5", + "windows-sys 0.59.0", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags", + "debugid", + "fxhash", + "serde", + "serde_json", +] + +[[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.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[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.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", + "serde", +] + +[[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 0.2.0", + "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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[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.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +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.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix 1.1.5", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "night-compiler" +version = "0.1.0" +dependencies = [ + "log", + "rustc-hash", + "waffle", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", +] + +[[package]] +name = "night-guest" +version = "0.1.0" +dependencies = [ + "night-compiler", + "night-snapshot", +] + +[[package]] +name = "night-snapshot" +version = "0.1.0" +dependencies = [ + "anyhow", + "night-compiler", + "rustc-hash", +] + +[[package]] +name = "nightmonkey" +version = "0.1.0" +dependencies = [ + "anyhow", + "libc", + "night-compiler", + "night-snapshot", + "waffle", + "wizer", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3325791708ad50580aeacfcce06cb5e462c9ba7a2368e109cb2012b944b70e" +dependencies = [ + "cranelift-bitset 0.118.0", + "log", + "wasmtime-math", +] + +[[package]] +name = "pulley-interpreter" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04adf8f93264685f2c70a3edb8f828c791e71b22659253319c33f29d1165aef" +dependencies = [ + "cranelift-bitset 0.132.3", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c120a6af1a574a42efcd9df2ad27cb78bc04118c5e0c69e90599f17301a1c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[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.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[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.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "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 1.1.5", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[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 = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + +[[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 1.1.5", + "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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[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 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[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 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[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_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[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 2.0.119", +] + +[[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 = "trait-variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b19a4867a870f6edc4c283f2b455804b1879c0baf0e642f26b03ed8ee262d9d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[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.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +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 = "waffle" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9d7881318c4957534037f7595778f89bc8aded2d853664358050bca3fb3882" +dependencies = [ + "anyhow", + "fxhash", + "log", + "rayon", + "smallvec", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +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.226.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d81b727619aec227dce83e7f7420d4e56c79acd044642a356ea045b98d4e13" +dependencies = [ + "leb128fmt", + "wasmparser 0.226.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.228.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d30290541f2d4242a162bbda76b8f2d8b1ac59eab3568ed6f2327d52c9b2c4" +dependencies = [ + "leb128fmt", + "wasmparser 0.228.0", +] + +[[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-encoder" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" +dependencies = [ + "leb128fmt", + "wasmparser 0.259.0", +] + +[[package]] +name = "wasm-jit-runner" +version = "0.1.0" +dependencies = [ + "anyhow", + "sha2", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", + "wasmtime 45.0.3", + "wasmtime-wasi 45.0.3", +] + +[[package]] +name = "wasmparser" +version = "0.226.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc28600dcb2ba68d7e5f1c3ba4195c2bddc918c0243fd702d0b6dbd05689b681" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.228.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + +[[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 = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.226.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "753a0516fa6c01756ee861f36878dfd9875f273aea9409d9ea390a333c5bcdc2" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.226.0", +] + +[[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 = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fe78033c72da8741e724d763daf1375c93a38bfcea99c873ee4415f6098c3f" +dependencies = [ + "addr2line 0.24.2", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "fxprof-processed-profile 0.6.0", + "gimli 0.31.1", + "hashbrown 0.15.5", + "indexmap", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "paste", + "postcard", + "psm", + "pulley-interpreter 31.0.0", + "rayon", + "rustix 0.38.44", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "sptr", + "target-lexicon", + "trait-variant", + "wasm-encoder 0.226.0", + "wasmparser 0.226.0", + "wasmtime-asm-macros", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ 31.0.0", + "wasmtime-fiber", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", + "wasmtime-math", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "wat", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a83ef84ac8bab735c89e27b0268b0586099fbe81281ae45be2b8e4f407b2daa" +dependencies = [ + "addr2line 0.26.1", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile 0.8.1", + "gimli 0.33.0", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.39.1", + "once_cell", + "postcard", + "pulley-interpreter 45.0.3", + "rayon", + "rustix 1.1.5", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", + "wasmtime-environ 45.0.3", + "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-asm-macros" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f3d44ae977d70ccf80938b371d5ec60b6adedf60800b9e8dd1223bb69f4cbc" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e209505770c7f38725513dba37246265fa6f724c30969de1e9d2a9e6c8f55099" +dependencies = [ + "anyhow", + "base64 0.21.7", + "directories-next", + "log", + "postcard", + "rustix 0.38.44", + "serde", + "serde_derive", + "sha2", + "toml 0.8.23", + "windows-sys 0.59.0", + "zstd", +] + +[[package]] +name = "wasmtime-component-macro" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397e68ee29eb072d8d8741c9d2c971a284cd1bc960ebf2c1f6a33ea6ba16d6e1" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.226.0", +] + +[[package]] +name = "wasmtime-component-util" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f292ef5eb2cf3d414c2bde59c7fa0feeba799c8db9a8c5a656ad1d1a1d05e10b" + +[[package]] +name = "wasmtime-cranelift" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52fc12eb8ea695a30007a4849a5fd56209dd86a15579e92e0c27c27122818505" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen 0.118.0", + "cranelift-control 0.118.0", + "cranelift-entity 0.118.0", + "cranelift-frontend 0.118.0", + "cranelift-native 0.118.0", + "gimli 0.31.1", + "itertools 0.12.1", + "log", + "object 0.36.7", + "pulley-interpreter 31.0.0", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.226.0", + "wasmtime-environ 31.0.0", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b6b4bf08e371edf262cccb62de10e214bd4aaafaa069f1cd49c9c1c3a5ae8e4" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bitset 0.118.0", + "cranelift-entity 0.118.0", + "gimli 0.31.1", + "indexmap", + "log", + "object 0.36.7", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.226.0", + "wasmparser 0.226.0", + "wasmprinter 0.226.0", + "wasmtime-component-util", +] + +[[package]] +name = "wasmtime-environ" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439686d3deb38525c86b88bbc90f7ba669f70c8edc7d6b35147c738bbef5ff76" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest 0.132.3", + "cranelift-bitset 0.132.3", + "cranelift-entity 0.132.3", + "gimli 0.33.0", + "hashbrown 0.17.1", + "indexmap", + "log", + "object 0.39.1", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", + "wasmprinter 0.248.0", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-fiber" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8828d7d8fbe90d087a9edea9223315caf7eb434848896667e5d27889f1173" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "rustix 0.38.44", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2799e6c35393c2a38999f13e4c80ab5d5d9756082bb554cbd1f60485a18293" +dependencies = [ + "base64 0.22.1", + "directories-next", + "log", + "postcard", + "rustix 1.1.5", + "serde", + "serde_derive", + "sha2", + "toml 0.9.12+spec-1.1.0", + "wasmtime-environ 45.0.3", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d2c5850fb8c448792453765d97f6f01096a32708f3c36798e86220763c90c0" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.248.0", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed79c4e9ab416dcc5e46b9d1ec9b7c2e3c730deab525996b0de45a35fc8b2c3" + +[[package]] +name = "wasmtime-internal-core" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3073c03f97f871fe6400e68621c863b93ba79296f8e570285231952d23fcc804" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a15981261d361f9c93b42929f446b4009840853ceea181ad6e17730f4016a0b" +dependencies = [ + "cfg-if", + "cranelift-codegen 0.132.3", + "cranelift-control 0.132.3", + "cranelift-entity 0.132.3", + "cranelift-frontend 0.132.3", + "cranelift-native 0.132.3", + "gimli 0.33.0", + "itertools 0.14.0", + "log", + "object 0.39.1", + "pulley-interpreter 45.0.3", + "smallvec", + "target-lexicon", + "thiserror 2.0.20", + "wasmparser 0.248.0", + "wasmtime-environ 45.0.3", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33aed9d91d54c9f861ba7e3c3130bded8a7a63e01fe58c3a9a36b45d6ee0fb57" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.5", + "wasmtime-environ 45.0.3", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944942118aab67e7b4febfe88a65d0af4cfd10af8ed0e79fd6cf39d75359ab7e" +dependencies = [ + "cc", + "object 0.39.1", + "rustix 1.1.5", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37dee05e8c35759826f6b926cd949c51f771b6a58619d8e60c63c3f3d3e5e59b" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a83f7ebe911a6f1605ff0d3a678472ddb1fcc57c3e2f6bae5dd4d170b625b3ed" +dependencies = [ + "cfg-if", + "cranelift-codegen 0.132.3", + "log", + "object 0.39.1", + "wasmtime-environ 45.0.3", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75980644456dc003238766254b0e5f002704630237ccd0728747991fe2739d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81af36995b4720b32e3d667541b548a27184a859d09a6ee745eaba095f5a6f6e" +dependencies = [ + "cranelift-codegen 0.132.3", + "gimli 0.33.0", + "log", + "object 0.39.1", + "target-lexicon", + "wasmparser 0.248.0", + "wasmtime-environ 45.0.3", + "wasmtime-internal-cranelift", + "winch-codegen 45.0.3", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada06667b7948892703fcc9f0b9b337c2e78c334d74d4fa62e2f75f07fbb3659" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser 0.248.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9eff86dedd48b023199de2d266f5d3e37bc7c5bafdc1e3e3057214649ecf5a" +dependencies = [ + "cc", + "object 0.36.7", + "rustix 0.38.44", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54f6c6c7e9d7eeee32dfcc10db7f29d505ee7dd28d00593ea241d5f70698e64" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-math" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1108aad2e6965698f9207ea79b80eda2b3dcc57dcb69f4258296d4664ae32cd" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-slab" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d6a321317281b721c5530ef733e8596ecc6065035f286ccd155b3fa8e0ab2f" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5732a5c86efce7bca121a61d8c07875f6b85c1607aa86753b40f7f8bd9d3a780" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-wasi" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b425ede2633fade96bd624b6f35cea5f8be1995d149530882dbc35efbf1e31f" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 0.38.44", + "system-interface", + "thiserror 1.0.69", + "tokio", + "tracing", + "url", + "wasmtime 31.0.0", + "wasmtime-wasi-io 31.0.0", + "wiggle 31.0.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-wasi" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e9db77f7b60f4c5f6f72847f55eb646ed7ed2f68e362559ee07dc543fb408" +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 0.10.3", + "rustix 1.1.5", + "thiserror 2.0.20", + "tokio", + "tracing", + "url", + "wasmtime 45.0.3", + "wasmtime-wasi-io 45.0.3", + "wiggle 45.0.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ec650d8891ec5ff823bdcefe3b370278becd1f33125bcfdcf628943dcde676" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures", + "wasmtime 31.0.0", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3805f02be91374a4fd02c0f947daf07bbaa6fbebb5909de5305fa5b0e9568f30" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime 45.0.3", +] + +[[package]] +name = "wasmtime-winch" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa4741ee66a52e2f0ec5f79040017123ba47d2dff9d994b35879cc2b7f468d4" +dependencies = [ + "anyhow", + "cranelift-codegen 0.118.0", + "gimli 0.31.1", + "object 0.36.7", + "target-lexicon", + "wasmparser 0.226.0", + "wasmtime-cranelift", + "wasmtime-environ 31.0.0", + "winch-codegen 31.0.0", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505c13fa0cac6c43e805347acf1e916c8de54e3790f2c22873c5692964b09b62" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "wit-parser 0.226.0", +] + +[[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 = "259.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69beba8d9da07af9a0971b559149a2ac4729895b55144de4fddbf5a02660648" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.259.0", +] + +[[package]] +name = "wat" +version = "1.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6eec44b0c80391b20fb7ad9ea440c72c5d382385bdf74444bb18321c425017e" +dependencies = [ + "wast 259.0.0", +] + +[[package]] +name = "wiggle" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc9a83fe01faa51423fc84941cdbe0ec33ba1e9a75524a560a27a4ad1ff2c3b" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "thiserror 1.0.69", + "tracing", + "wasmtime 31.0.0", + "wiggle-macro 31.0.0", +] + +[[package]] +name = "wiggle" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aca130e25a57e29bbb541441b6e261970a8469580bffebe904d96f0df67e41d" +dependencies = [ + "bitflags", + "thiserror 2.0.20", + "tracing", + "wasmtime 45.0.3", + "wasmtime-environ 45.0.3", + "wiggle-macro 45.0.3", +] + +[[package]] +name = "wiggle-generate" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d250c01cd52cfdb40aad167fad579af55acbeccb85a54827099d31dc1b90cbd7" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "shellexpand", + "syn 2.0.119", + "witx", +] + +[[package]] +name = "wiggle-generate" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f0a45b47e893521cad81819f2e0db18a8b05086fd4bfb35369ed8830a8f0148" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-environ 45.0.3", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35be0aee84be808a5e17f6b732e110eb75703d9d6e66e22c7464d841aa2600c5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "wiggle-generate 31.0.0", +] + +[[package]] +name = "wiggle-macro" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e297e0325cffa6d368386b6e672e9d6c6a7ad34bdb8a5f49319db4decef2a990" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "wiggle-generate 45.0.3", +] + +[[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 = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f05457f74ec3c94d5c5caac06b84fd8d9d4d7fa21419189845ed245a53477" +dependencies = [ + "anyhow", + "cranelift-codegen 0.118.0", + "gimli 0.31.1", + "regalloc2 0.11.2", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.226.0", + "wasmtime-cranelift", + "wasmtime-environ 31.0.0", +] + +[[package]] +name = "winch-codegen" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73bc25d27222248f9bafc7e9945f61e74275360c3ea0d34008810d436140a53" +dependencies = [ + "cranelift-assembler-x64 0.132.3", + "cranelift-codegen 0.132.3", + "gimli 0.33.0", + "regalloc2 0.15.2", + "smallvec", + "target-lexicon", + "thiserror 2.0.20", + "wasmparser 0.248.0", + "wasmtime-environ 45.0.3", + "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 2.0.119", +] + +[[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 2.0.119", +] + +[[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" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[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-parser" +version = "0.226.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f007722bfd43a2978c5b8b90f02c927dddf0f11c5f5b50929816b3358718cd" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.226.0", +] + +[[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 = "wizer" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aee5402e9c06fb330c2edd1d7ae4bbdc9214b2cd19774199c9f6ec313d0237a" +dependencies = [ + "anyhow", + "cap-std", + "log", + "rayon", + "wasm-encoder 0.228.0", + "wasmparser 0.228.0", + "wasmtime 31.0.0", + "wasmtime-wasi 31.0.0", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ef6a0f1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[workspace] +resolver = "2" +members = [ + "compiler", + "snapshot", + "nightmonkey", + "wasm-jit-runner", + "guest", +] + +[workspace.package] +rust-version = "1.82.0" +authors = ["Chris Fallin "] +license = "Apache-2.0 WITH LLVM-exception" + +# The host tools are built by CMake (see CMakeLists.txt), which also checks +# the checked-in opcode table (compiler/src/opcodes/) against the engine +# being built against; a plain `cargo build` needs nothing from the engine. +[profile.release] +debug = false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f9d8195 --- /dev/null +++ b/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/ORG_CODE_OF_CONDUCT.md b/ORG_CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6f4fb3f --- /dev/null +++ b/ORG_CODE_OF_CONDUCT.md @@ -0,0 +1,143 @@ +# Bytecode Alliance Organizational Code of Conduct (OCoC) + +*Note*: this Code of Conduct pertains to organizations' behavior. Please also see the [Individual Code of Conduct](CODE_OF_CONDUCT.md). + +## Preamble + +The Bytecode Alliance (BA) welcomes involvement from organizations, +including commercial organizations. This document is an +*organizational* code of conduct, intended particularly to provide +guidance to commercial organizations. It is distinct from the +[Individual Code of Conduct (ICoC)](CODE_OF_CONDUCT.md), and does not +replace the ICoC. This OCoC applies to any group of people acting in +concert as a BA member or as a participant in BA activities, whether +or not that group is formally incorporated in some jurisdiction. + +The code of conduct described below is not a set of rigid rules, and +we did not write it to encompass every conceivable scenario that might +arise. For example, it is theoretically possible there would be times +when asserting patents is in the best interest of the BA community as +a whole. In such instances, consult with the BA, strive for +consensus, and interpret these rules with an intent that is generous +to the community the BA serves. + +While we may revise these guidelines from time to time based on +real-world experience, overall they are based on a simple principle: + +*Bytecode Alliance members should observe the distinction between + public community functions and private functions — especially + commercial ones — and should ensure that the latter support, or at + least do not harm, the former.* + +## Guidelines + + * **Do not cause confusion about Wasm standards or interoperability.** + + Having an interoperable WebAssembly core is a high priority for + the BA, and members should strive to preserve that core. It is fine + to develop additional non-standard features or APIs, but they + should always be clearly distinguished from the core interoperable + Wasm. + + Treat the WebAssembly name and any BA-associated names with + respect, and follow BA trademark and branding guidelines. If you + distribute a customized version of software originally produced by + the BA, or if you build a product or service using BA-derived + software, use names that clearly distinguish your work from the + original. (You should still provide proper attribution to the + original, of course, wherever such attribution would normally be + given.) + + Further, do not use the WebAssembly name or BA-associated names in + other public namespaces in ways that could cause confusion, e.g., + in company names, names of commercial service offerings, domain + names, publicly-visible social media accounts or online service + accounts, etc. It may sometimes be reasonable, however, to + register such a name in a new namespace and then immediately donate + control of that account to the BA, because that would help the project + maintain its identity. + + For further guidance, see the BA Trademark and Branding Policy + [TODO: create policy, then insert link]. + + * **Do not restrict contributors.** If your company requires + employees or contractors to sign non-compete agreements, those + agreements must not prevent people from participating in the BA or + contributing to related projects. + + This does not mean that all non-compete agreements are incompatible + with this code of conduct. For example, a company may restrict an + employee's ability to solicit the company's customers. However, an + agreement must not block any form of technical or social + participation in BA activities, including but not limited to the + implementation of particular features. + + The accumulation of experience and expertise in individual persons, + who are ultimately free to direct their energy and attention as + they decide, is one of the most important drivers of progress in + open source projects. A company that limits this freedom may hinder + the success of the BA's efforts. + + * **Do not use patents as offensive weapons.** If any BA participant + prevents the adoption or development of BA technologies by + asserting its patents, that undermines the purpose of the + coalition. The collaboration fostered by the BA cannot include + members who act to undermine its work. + + * **Practice responsible disclosure** for security vulnerabilities. + Use designated, non-public reporting channels to disclose technical + vulnerabilities, and give the project a reasonable period to + respond, remediate, and patch. [TODO: optionally include the + security vulnerability reporting URL here.] + + Vulnerability reporters may patch their company's own offerings, as + long as that patching does not significantly delay the reporting of + the vulnerability. Vulnerability information should never be used + for unilateral commercial advantage. Vendors may legitimately + compete on the speed and reliability with which they deploy + security fixes, but withholding vulnerability information damages + everyone in the long run by risking harm to the BA project's + reputation and to the security of all users. + + * **Respect the letter and spirit of open source practice.** While + there is not space to list here all possible aspects of standard + open source practice, some examples will help show what we mean: + + * Abide by all applicable open source license terms. Do not engage + in copyright violation or misattribution of any kind. + + * Do not claim others' ideas or designs as your own. + + * When others engage in publicly visible work (e.g., an upcoming + demo that is coordinated in a public issue tracker), do not + unilaterally announce early releases or early demonstrations of + that work ahead of their schedule in order to secure private + advantage (such as marketplace advantage) for yourself. + + The BA reserves the right to determine what constitutes good open + source practices and to take action as it deems appropriate to + encourage, and if necessary enforce, such practices. + +## Enforcement + +Instances of organizational behavior in violation of the OCoC may +be reported by contacting the Bytecode Alliance CoC team at +[report@bytecodealliance.org](mailto:report@bytecodealliance.org). The +CoC team will review and investigate all complaints, and will respond +in a way that it deems appropriate to the circumstances. The CoC team +is obligated to maintain confidentiality with regard to the reporter of +an incident. Further details of specific enforcement policies may be +posted separately. + +When the BA deems an organization in violation of this OCoC, the BA +will, at its sole discretion, determine what action to take. The BA +will decide what type, degree, and duration of corrective action is +needed, if any, before a violating organization can be considered for +membership (if it was not already a member) or can have its membership +reinstated (if it was a member and the BA canceled its membership due +to the violation). + +In practice, the BA's first approach will be to start a conversation, +with punitive enforcement used only as a last resort. Violations +often turn out to be unintentional and swiftly correctable with all +parties acting in good faith. diff --git a/README.md b/README.md index 22aab7a..eea23d4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,304 @@ # NightMonkey: AOT JavaScript-to-WebAssembly compilation -Initially empty repository as a base for code review. +**NightMonkey** is an ahead-of-time JS-to-Wasm compilation tier layered +on top of 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`. | +| `compiler/src/opcodes/` | The `JSOp` enum, lengths and stack effects, generated from the engine's `vm/Opcodes.h` by `scripts/gen_opcodes.py` and checked in per engine version (`ff147.rs`, ...); a cargo feature of the same name selects one. | +| `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. `NightHooks.cpp` is the engine's external compiler hook table (see `docs/INTEGRATION.md`). Plus the value stack and snapshot registration/activation/capture. Built against SpiderMonkey's private headers. | +| `shell/` | `nightshell.cpp`: the NightMonkey wasm shell, SpiderMonkey's shell (`libjsshell`) with the hooks installed and the `--night-snapshot` / `--night-inprocess` flows. | +| `guest/` | `night-guest`: the compiler and snapshot reader as one wasm staticlib for the in-process lane. | +| `spidermonkey/` | The mozconfig that builds SpiderMonkey the way NightMonkey needs it. | +| `scripts/` | `build-spidermonkey.sh`, `run-jit-tests.sh`, `run-jstests.sh`, and the harness shell wrapper template. | +| `tests/` | The jit-test and jstests exclusion lists for the AOT lane. | +| `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/` | Benchmark-lane mozconfigs for the SpiderMonkey tree (`mozconfig-native`/`-ion`/`-wasm`/`-weval`); the NightMonkey build itself uses `spidermonkey/mozconfig`. | +| `docs/` | `DESIGN.md`, `INTEGRATION.md` (the SpiderMonkey hook surface), `TODO`. | +| `tools/` | Profiling, benchmarking, and visualization helpers (`viz.py`, `opprof.py`, `pairab.sh`, ...). | + +## Building + +NightMonkey lives in its own repository and links against a SpiderMonkey +build. SpiderMonkey needs a small patch: `--enable-external-compiler-hooks`, +which adds the per-object/per-script words and the hook table the tier plugs +into, exports the engine's private headers, and builds the shell as a static +library (`docs/INTEGRATION.md`). + +Step 1 -- build SpiderMonkey (wasm32-wasi, wizer-capable shell, hooks on): + +``` +scripts/build-spidermonkey.sh /path/to/firefox +# -> /path/to/firefox/obj-nightmonkey-sm/dist +``` + +This is `MOZCONFIG=spidermonkey/mozconfig ./mach build` in the SpiderMonkey +checkout. `dist/include-private/` mirrors `js/src` (source and generated +headers) and records the compile flags libjs was built with in +`js-build-config.json`; `dist/lib/` holds `libjsshell.a`, `libjs_static.a`, +`libjsrust.a` and `libpure_virtual.a`. + +Step 2 -- build NightMonkey against it: + +``` +cmake -S . -B build -DSPIDERMONKEY_DIST=/path/to/firefox/obj-nightmonkey-sm/dist +cmake --build build +# -> build/bin/js (the NightMonkey wasm32-wasi shell) +# -> build/bin/js-inproc (the same, with the in-process compiler) +# -> build/bin/nightmonkey (the host AOT compiler) +# -> build/bin/wasm-jit-runner (the in-process test host) +# -> build/bin/inproc-shell.sh (the harness wrapper) +``` + +The CMake build compiles the runtime and the shell with exactly the flags +libjs used (same sysroot, target, ABI flags and force-included configuration +headers), and drives Cargo for the host tools and the guest staticlib. +Options: `-DNIGHT_INPROCESS=OFF` drops the in-process lane (no guest +compiler, no runner); `-DNIGHT_DEBUG=ON` turns on the runtime diagnostics and +the crash-on-failure of the in-process lane; `-DNIGHT_ENGINE_VERSION=ff147` +selects the engine version (below). + +Plain `cargo build` works for the Rust crates. + +### Engine versions + +The compiler's opcode table (`JSOp`, opcode lengths and stack effects) +is generated from the engine's `vm/Opcodes.h` by +`scripts/gen_opcodes.py` and checked in under `compiler/src/opcodes/`, +one file per supported engine version, each recording a digest of the +header it came from. A cargo feature named after the version (e.g., +`ff147`) selects the table a build targets, and the lowerings +feature-guard any per-version differences. The CMake build checks that +the selected table is what the engine being built against generates: + +``` +scripts/gen_opcodes.py check ff147 /path/to/firefox/obj-nightmonkey-sm/dist/include-private/vm/Opcodes.h +``` + +so an engine whose bytecode differs from the table, in shape or in +meaning, forces a build failure. To support a new engine: review its +`Opcodes.h` diff against the lowerings, then (e.g. for version +`ff153`) + +``` +scripts/gen_opcodes.py generate ff153 /path/to/that/Opcodes.h +``` + +declare the `ff153` feature in `compiler/Cargo.toml` and the crates +that forward it (`snapshot`, `nightmonkey`, `guest`), add its arm to +`compiler/src/opcodes/mod.rs`, and condition the lowering changes on +`feature = "ff153"`. + +CI (`.github/workflows/ci.yml`) builds both trees and runs the jit-test +suite in the AOT lane. + +## Prerequisites + +- A SpiderMonkey checkout on the tracked branch, bootstrapped for the JS + shell (`./mach --no-interactive bootstrap --application-choice=js`) plus + the wasm32-wasi sysroot in `~/.mozbuild` (see `.github/workflows/ci.yml` + for the exact steps). +- A Rust toolchain with the `wasm32-wasip1` target + (`rustup target add wasm32-wasip1`), CMake 3.20+. +- `wasmtime` on `$PATH` or at `$HOME/bin/wasmtime`, to run compiled modules. + +Wizer is a library dependency of `nightmonkey`; there is nothing to install. + +## Flow 1: snapshot (the shipping flow) + +To compile a program: + +``` +build/bin/nightmonkey --shell build/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 and appends 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 build/bin/js 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) + +Run a program: + +``` +build/bin/wasm-jit-runner --dir / --cache-dir ~/.cache/wjr \ + build/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. + +The jit-test suite in both lanes, from the SpiderMonkey checkout's harness: + +``` +scripts/run-jit-tests.sh /path/to/firefox build -- -j16 +NIGHT_INPROCESS_OFF=1 scripts/run-jit-tests.sh /path/to/firefox build -- -j16 +``` + +`scripts/run-jstests.sh` does the same for jstests (hours for the full +suite; append a path to scope). Both lanes are expected to pass completely. +Both lanes skip `tests/wasi-jit-test-excludes.txt` and +`tests/wasi-jstests-excludes.txt`: tests the wasm32-wasi shell cannot run at +all (no Intl, no shared memory or Atomics, no threads, no time zone database, +a small native stack), independent of the tier. The AOT lane additionally +skips `tests/jit-test-excludes.txt` and `tests/jstests-excludes.txt` (passed +as `--exclude-from` / `--exclude-file`, so the same tests still run in the +baseline lane): 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 a comment). The SpiderMonkey tree carries +no test annotations for NightMonkey. NightMonkey's own regression tests +(`tests/jit-test/`) run in both lanes with `scripts/run-night-tests.sh`. + +## Build-system notes + +- The in-process lane links two Rust static libraries into the shell: + SpiderMonkey's `libjsrust.a` and NightMonkey's `libnight_guest.a`. Both + are built by the same toolchain against the prebuilt `wasm32-wasip1` + standard library, so their std objects are identical and the linker keeps + one copy. +- Layout facts the compiler bakes into generated code are pinned by + `static_assert`s in `runtime/NightInlineHeap.cpp` against the engine + headers, and the snapshot reader checks the registration block's ABI + version and layout descriptor at runtime, so a SpiderMonkey upgrade that + moves a field fails to build or refuses to compile rather than + miscompiling. The opcode table is checked in per engine version and + verified against the engine's `Opcodes.h` by every build (see "Engine + versions"). + +For performance work, the benchmark-lane configs +(`mozconfig-native`/`-ion`/`-wasm`/`-weval`) live in `configs/`. + +## 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)**: the SpiderMonkey side: + the `--enable-external-compiler-hooks` surface NightMonkey plugs into, + organized by mechanism, and how to port it to another SpiderMonkey. +- **[`docs/TODO`](docs/TODO)**. + +## 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/compiler/Cargo.toml b/compiler/Cargo.toml new file mode 100644 index 0000000..7c3cc3e --- /dev/null +++ b/compiler/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "night-compiler" +version = "0.1.0" +edition = "2021" +rust-version.workspace = true +authors.workspace = true +license.workspace = true + +[features] +# The engine version whose opcode table (compiler/src/opcodes/.rs) +# the compiler is built for: exactly one. +default = ["ff147"] +ff147 = [] + +[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"] } + +[dev-dependencies] +wasm-encoder = "0.248" diff --git a/compiler/build.rs b/compiler/build.rs new file mode 100644 index 0000000..9fbf00d --- /dev/null +++ b/compiler/build.rs @@ -0,0 +1,223 @@ +// The runtime's derivatives of the ABI (region table, region shape) +// are generated here from the runtime headers in this tree. The +// engine's opcode table is not: it is generated ahead of time by +// scripts/gen_opcodes.py and checked in under src/opcodes/, one file +// per engine version. +fn main() { + let out_dir = std::env::var("OUT_DIR").unwrap(); + + write_env_regions(&out_dir); + write_region_shape(&out_dir); +} + +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 +} diff --git a/compiler/night-compiler.h b/compiler/night-compiler.h new file mode 100644 index 0000000..5788404 --- /dev/null +++ b/compiler/night-compiler.h @@ -0,0 +1,227 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: */ + +#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/compiler/src/bytecode.rs b/compiler/src/bytecode.rs new file mode 100644 index 0000000..9deeb6e --- /dev/null +++ b/compiler/src/bytecode.rs @@ -0,0 +1,1075 @@ +use crate::ids::Pc; +use crate::source::SourceObjectId; + +pub use crate::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/compiler/src/constants.rs b/compiler/src/constants.rs new file mode 100644 index 0000000..2e84f0d --- /dev/null +++ b/compiler/src/constants.rs @@ -0,0 +1,422 @@ +//! 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/compiler/src/disasm.rs b/compiler/src/disasm.rs new file mode 100644 index 0000000..0f4d5bd --- /dev/null +++ b/compiler/src/disasm.rs @@ -0,0 +1,210 @@ +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/compiler/src/env_regions.rs b/compiler/src/env_regions.rs new file mode 100644 index 0000000..d55de7a --- /dev/null +++ b/compiler/src/env_regions.rs @@ -0,0 +1,10 @@ +//! 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/compiler/src/facts.rs b/compiler/src/facts.rs new file mode 100644 index 0000000..4293a6f --- /dev/null +++ b/compiler/src/facts.rs @@ -0,0 +1,559 @@ +//! 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/compiler/src/ids.rs b/compiler/src/ids.rs new file mode 100644 index 0000000..b3642a7 --- /dev/null +++ b/compiler/src/ids.rs @@ -0,0 +1,472 @@ +//! 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/compiler/src/lib.rs b/compiler/src/lib.rs new file mode 100644 index 0000000..6028d01 --- /dev/null +++ b/compiler/src/lib.rs @@ -0,0 +1,234 @@ +// 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 opcodes; +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/compiler/src/likelier/builtins.rs b/compiler/src/likelier/builtins.rs new file mode 100644 index 0000000..ad529f2 --- /dev/null +++ b/compiler/src/likelier/builtins.rs @@ -0,0 +1,617 @@ +//! 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/compiler/src/likelier/calls.rs b/compiler/src/likelier/calls.rs new file mode 100644 index 0000000..24c56f0 --- /dev/null +++ b/compiler/src/likelier/calls.rs @@ -0,0 +1,1031 @@ +//! 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/compiler/src/likelier/dump.rs b/compiler/src/likelier/dump.rs new file mode 100644 index 0000000..771d7f7 --- /dev/null +++ b/compiler/src/likelier/dump.rs @@ -0,0 +1,280 @@ +//! 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/compiler/src/likelier/effects.rs b/compiler/src/likelier/effects.rs new file mode 100644 index 0000000..dc4b7e6 --- /dev/null +++ b/compiler/src/likelier/effects.rs @@ -0,0 +1,597 @@ +//! 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/compiler/src/likelier/emit.rs b/compiler/src/likelier/emit.rs new file mode 100644 index 0000000..8426432 --- /dev/null +++ b/compiler/src/likelier/emit.rs @@ -0,0 +1,2915 @@ +//! `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/compiler/src/likelier/engine.rs b/compiler/src/likelier/engine.rs new file mode 100644 index 0000000..73f9c4b --- /dev/null +++ b/compiler/src/likelier/engine.rs @@ -0,0 +1,1079 @@ +//! 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/compiler/src/likelier/heap.rs b/compiler/src/likelier/heap.rs new file mode 100644 index 0000000..9c8fa3e --- /dev/null +++ b/compiler/src/likelier/heap.rs @@ -0,0 +1,2750 @@ +//! 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/wasm-jit-runner/.gitignore b/wasm-jit-runner/.gitignore new file mode 100644 index 0000000..1b63c34 --- /dev/null +++ b/wasm-jit-runner/.gitignore @@ -0,0 +1,2 @@ +/target +/guest/example/test_guest.wasm diff --git a/wasm-jit-runner/Cargo.lock b/wasm-jit-runner/Cargo.lock new file mode 100644 index 0000000..6ba28dd --- /dev/null +++ b/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/wasm-jit-runner/Cargo.toml b/wasm-jit-runner/Cargo.toml new file mode 100644 index 0000000..b826c57 --- /dev/null +++ b/wasm-jit-runner/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "wasm-jit-runner" +version = "0.1.0" +edition = "2021" +authors.workspace = true +license.workspace = true + +[dependencies] +anyhow = "1" +wasmtime = "45" +wasmtime-wasi = "45" +wasm-encoder = { version = "0.252", features = ["wasmparser"] } +wasmparser = "0.252" +sha2 = "0.10" diff --git a/wasm-jit-runner/README.md b/wasm-jit-runner/README.md new file mode 100644 index 0000000..7631083 --- /dev/null +++ b/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`). + + +## NightMonkey extensions (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/wasm-jit-runner/guest/example/build.sh b/wasm-jit-runner/guest/example/build.sh new file mode 100755 index 0000000..b047ac7 --- /dev/null +++ b/wasm-jit-runner/guest/example/build.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# 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/wasm-jit-runner/guest/example/test_guest.c b/wasm-jit-runner/guest/example/test_guest.c new file mode 100644 index 0000000..a281473 --- /dev/null +++ b/wasm-jit-runner/guest/example/test_guest.c @@ -0,0 +1,154 @@ +/* + * 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/wasm-jit-runner/guest/wasm_add.h b/wasm-jit-runner/guest/wasm_add.h new file mode 100644 index 0000000..f19b9c6 --- /dev/null +++ b/wasm-jit-runner/guest/wasm_add.h @@ -0,0 +1,78 @@ +/* + * 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/wasm-jit-runner/guest/wasm_build.h b/wasm-jit-runner/guest/wasm_build.h new file mode 100644 index 0000000..317cd7d --- /dev/null +++ b/wasm-jit-runner/guest/wasm_build.h @@ -0,0 +1,233 @@ +/* + * 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/wasm-jit-runner/src/addfuncs.rs b/wasm-jit-runner/src/addfuncs.rs new file mode 100644 index 0000000..ced7ebd --- /dev/null +++ b/wasm-jit-runner/src/addfuncs.rs @@ -0,0 +1,528 @@ +//! 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/wasm-jit-runner/src/cache.rs b/wasm-jit-runner/src/cache.rs new file mode 100644 index 0000000..dec4e4f --- /dev/null +++ b/wasm-jit-runner/src/cache.rs @@ -0,0 +1,60 @@ +//! 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/wasm-jit-runner/src/main.rs b/wasm-jit-runner/src/main.rs new file mode 100644 index 0000000..90d9e2e --- /dev/null +++ b/wasm-jit-runner/src/main.rs @@ -0,0 +1,191 @@ +//! 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/wasm-jit-runner/src/modedit.rs b/wasm-jit-runner/src/modedit.rs new file mode 100644 index 0000000..4361351 --- /dev/null +++ b/wasm-jit-runner/src/modedit.rs @@ -0,0 +1,206 @@ +//! 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/wasm-jit-runner/test.sh b/wasm-jit-runner/test.sh new file mode 100755 index 0000000..87bd1eb --- /dev/null +++ b/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